为什么添加默认的移动赋值运算符会破坏标准交换函数的编译?

Why adding a defaulted move assignment operator breaks compilation of standard swap function?

提问人:Xeverous 提问时间:9/15/2023 最后编辑:Xeverous 更新时间:9/15/2023 访问量:77

问:

在下面的代码中,如果未注释移动赋值,则交换函数将停止程序的编译。我在所有 3 个主要编译器(GCC、Clang、MSVC)上都观察到了这种行为。

#include <utility>
#include <memory>

struct test
{
    test() = default;

    test(test&& other) noexcept = default;
    //test& operator=(test&& other) noexcept = default;

    test(const test& other)
    : ptr(std::make_unique<int>(*other.ptr))
    {}

    test& operator=(test other) noexcept
    {
        std::swap(*this, other);
        return *this;
    }

    std::unique_ptr<int> ptr;
};

Godbolt 测试:https://godbolt.org/z/v1hGzzEaz

在研究标准库实现时,它们使用 SFINAE 或概念来启用/禁用重载,并且当特殊函数未注释时,由于某种原因,某些特征会失败(和/或在 libstdc++ 上)。std::swapis_move_constructibleis_move_assignable

我的问题是:为什么添加默认的特殊成员函数会阻止标准库将类型视为可移动的?

编辑 1:事实证明,问题在于 x 值在 和 之间的重载解析中没有偏好(这会导致不明确的重载错误),因此标准库特征无法将类型识别为可移动的。TT&&

编辑 2:重要提示:请注意,示例代码不是复制和交换习语的正确实现。它应该使用自定义交换函数或使用所有 5 个特殊成员函数来完成。当前实现创建移动/复制分配和交换调用的无限递归。

C++ move-semantics raii

评论

0赞 Botje 9/15/2023
您是否在实际代码中也拼写了按值而不是常量引用?test& operator=(test other)other
2赞 HolyBlackCat 9/15/2023
我很想以错别字结束。 是根据 move-ctor 和 move-assignment 实现的,你在这里得到一个无限递归。它应该是.std::swap(*this, other);std::swap(ptr, other.ptr);
2赞 HolyBlackCat 9/15/2023
@Botje 不,这是复制和交换成语。按价值取值是有意为之的,并且有一些好处。
1赞 HolyBlackCat 9/15/2023
旁注:copy&swap 应该是 .operator=noexcept
0赞 Xeverous 9/15/2023
@HolyBlackCat添加了缺失,但编译问题没有任何变化。noexcept

答:

5赞 Jan Schultke 9/15/2023 #1

的主要实现在内部使用移动赋值,如下所示:std::swap

template <typename T>
  requires std::is_swappable_v<T> // exposition-only, constraint might be different
void swap(T& a, T& b) {
    T c = std::move(a);
    a = std::move(b);
    b = std::move(c);
}

这意味着移动分配需要对您的类型有效,但事实并非如此。 如果要调用移动赋值运算符,则会出现错误:

<source>:18:15: error: use of overloaded operator '=' is ambiguous [...]
[...] |
<source>:9:11: note: candidate function
    9 |     test& operator=(test&& other) noexcept = default;
      |           ^
<source>:15:11: note: candidate function
   15 |     test& operator=(test other);
      |           ^

std::swap 受到约束,因此只能交换 MoveAssignable 类型,而不能交换您的类型。 两个运算符都可以用 xvalue 调用,并且两者都不是更好的匹配。 这是因为只有左值到右值的转换会延长转换序列,并且从 xvalue 初始化对象被认为是“自由的”(例如,这不是转换,也不比绑定引用差)。=

即使你可以打电话,你也不能同时打电话std::swap

  • 依赖 的默认实现,它使用std::swapoperator=
  • 根据operator=std::swap

这将是无限递归,作为 和 的定义是循环的。=std::swap

解决 方案

您可以为您的类型定义自定义,以便不再依赖 . 仅保留 ,如下所示:swapstd::swapoperator=(test)

friend void swap(test& a, test& b) noexcept {
    using std::swap;    // Use swap() functions found through ADL, fall back
    swap(a.ptr, b.ptr); // onto std::swap if there are none.
}                       // (not necessary if you only have a std::unique_ptr member,
                        // you could just use std::swap(a.ptr, b.ptr)).

test& operator=(test other) noexcept {
    swap(*this, other); // custom function, not std::swap
    return *this;
}

您还可以单独和手动定义,以便使用移动赋值运算符,并且在重载解析中不会有歧义。operator=(const test&)operator=(test&&)std::swap

评论

0赞 Xeverous 9/15/2023
“两个运算符都可以用 xvalue 调用,而且两者都不是更好的匹配。”这是令人惊讶的。我预计 lvalue 会进入 const lvalue 引用重载,而两个 rvalue(prvalue 和 xvalue)都会进入 rvalue 引用重载。
0赞 HolyBlackCat 9/15/2023
@Xeverous 但是您没有右值引用重载。
0赞 Xeverous 9/15/2023
@HolyBlackCat我在未注释重载时这样做。然后代码中断。
0赞 HolyBlackCat 9/15/2023
对不起,我今天显然看不懂。我的意思是没有超载。Only 和 .const T &TT &&
0赞 Xeverous 9/15/2023
@HolyBlackCat确实如此。对我来说,xvalues 没有偏好仍然让我感到惊讶。