移动赋值运算符

来自cppreference.com
< cpp‎ | language

T 的移动赋值运算符是名为 operator=的非模板非静态成员函数,它接受恰好一个 T&&const T&&volatile T&&const volatile T&& 类型的形参显式对象形参除外) (C++23 起)

语法

类名 & 类名 ::operator=(类名 &&) (1) (C++11 起)
类名 & 类名 ::operator=(类名 &&) = default; (2) (C++11 起)
类名 & 类名 ::operator=(类名 &&) = delete; (3) (C++11 起)

解释

1) 移动赋值运算符的典型声明。
2) 强制编译器生成移动赋值运算符。
3) 避免隐式移动赋值。

每当重载决议选择移动赋值运算符时,它都会被调用,例如当对象出现在赋值表达式左侧,而它的右侧是同类型或可隐式转换的类型的右值时。

典型的移动赋值运算符“窃取”实参曾保有的资源(例如指向动态分配对象的指针,文件描述符,TCP socket,输入输出流,运行的线程,等等),而非复制它们,并使得实参遗留在某个合法但不确定的状态。例如,从 std::string 或从 std::vector 移动赋值可能导致实参被置空。然而这并不保证会发生。移动赋值与普通赋值相比,它的定义较为宽松而非更严格;在完成时,普通赋值必须留下数据的两份副本,而移动赋值只要求留下一份。

隐式声明的移动赋值运算符

如果没有对类类型提供任何用户定义的移动赋值运算符,且满足下列所有条件:

  • 没有用户声明的复制构造函数;
  • 没有用户声明的移动构造函数;
  • 没有用户声明的复制赋值运算符;
  • 没有用户声明的析构函数,

那么编译器将声明一个,作为类的 inline public 成员,并拥有签名 T& T::operator=(T&&)

类可以拥有多个移动赋值运算符,如 T& T::operator=(const T&&)T& T::operator=(T&&)。当存在用户定义的移动赋值运算符时,用户仍然可以通过关键词 default 强迫编译器生成隐式声明的移动赋值运算符。

隐式声明(或在它的首个声明被预置)的移动赋值运算符具有动态异常说明 (C++17 前)noexcept 说明 (C++17 起)中所描述的异常说明。

因为每个类总是会声明赋值运算符(移动或复制),所以基类的赋值运算符始终被隐藏。当使用 using 声明从基类带入赋值运算符,且它的实参类型与派生类的隐式赋值运算符的实参类型相同时,该 using 声明也会被隐式声明隐藏。

弃置的隐式声明的移动赋值运算符

如果满足下列任一条件,那么类 T 的隐式声明或预置的移动赋值运算符被定义为弃置的

  • T 拥有 const 限定的非静态数据成员。
  • T 拥有引用类型的非静态数据成员。
  • T 拥有无法移动赋值(拥有被弃置、不可访问或有歧义的移动赋值运算符)的非静态数据成员或直接基类。

重载决议忽略被弃置的隐式声明的移动赋值运算符。

平凡的移动赋值运算符

如果满足下列所有条件,那么类 T 的移动赋值运算符是平凡的:

  • 它不是用户提供的(即它是隐式定义或预置的);
  • T 没有虚成员函数;
  • T 没有虚基类;
  • T 的每个直接基类选择的移动赋值运算符都是平凡的;
  • T 的每个类类型(或类类型的数组)的非静态数据成员选择的移动赋值运算符都是平凡的;

平凡移动赋值运算符实施与平凡复制赋值运算符相同的动作,即如同以 std::memmove 进行对象表示的复制。所有与 C 兼容的数据类型(POD 类型)都可以平凡移动。

合格的移动赋值运算符

没有被弃置的移动赋值运算符是合格的。

(C++20 前)

满足下列所有条件的移动赋值运算符是合格的:

  • 它没有被弃置,且
  • 满足它的所有关联约束(如果存在),且
  • 没有比它更受约束且拥有相同的第一形参类型和相同的 cv 或引用限定符(如果存在)的移动赋值运算符。
(C++20 起)

合格移动赋值运算符的平凡性确定该类是否为该类是否为可平凡复制类型

隐式定义的移动赋值运算符

如果隐式声明的移动赋值运算符既没有被弃置也不平凡,那么当它被 ODR 式使用或用于常量求值 (C++14 起)时,它会被编译器定义(即生成并编译函数体)。

对于联合体类型,隐式定义的移动赋值运算符(如用 std::memmove)复制它的对象表示。

对于非联合体类类型,移动赋值运算符按照声明顺序对对象的各直接基类和直接非静态成员进行完整的逐成员移动赋值,其中对标量用内建运算符,对数组用逐元素移动赋值,而对类类型用移动赋值运算符(非虚调用)。

如果满足下列所有条件,那么类 T 的隐式定义的复制赋值运算符是 constexpr 的:

  • T字面类型,且
  • 移动每个直接基类子对象时选中的赋值运算符都是 constexpr 函数,且
  • 移动 T 的每个类(或它的数组)类型的数据成员时选中的赋值运算符都是 constexpr 函数。
(C++14 起)
(C++23 前)

T 的隐式定义的复制赋值运算符是 constexpr 的。

(C++23 起)

与复制赋值一样,隐式定义的移动赋值运算符是否会多次对在继承网格中可通过多于一条路径访问的虚基类子对象赋值是未指明的:

struct V
{
    V& operator=(V&& other)
    {
        // 这可能会被调用一或两次
        // 如果调用两次,那么 'other' 是刚被移动的 V 子对象
        return *this;
    }
};
struct A : virtual V {}; // operator= 调用 V::operator=
struct B : virtual V {}; // operator= 调用 V::operator=
struct C : B, A {};      // operator= 调用 B::operator=,然后调用 A::operator=
                         // 但可能只调用一次 V::operator=
 
int main()
{
    C c1, c2;
    c2 = std::move(c1);
}

注解

如果复制和移动赋值运算符都有提供,那么重载决议会在实参是右值(例如无名临时量的纯右值std::move 的结果的亡值)时选择移动赋值,而在实参是左值(具名对象或返回左值引用的函数或运算符)时选择复制赋值。如果只提供了复制赋值,那么重载决议对于所有值类别都会选择它(只要它按值或按到 const 的引用接收它的实参),从而当移动赋值不可用时,复制赋值将会成为它的后备。

隐式定义的移动赋值运算符是否会多次对在继承网格中可通过多于一条路径访问的虚基类子对象赋值是未指明的(同样适用于复制赋值)。

有关用户定义的移动赋值运算符应当有哪些行为,见赋值运算符重载

示例

#include <string>
#include <iostream>
#include <utility>
 
struct A
{
    std::string s;
 
    A() : s("测试") {}
 
    A(const A& o) : s(o.s) { std::cout << "移动失败!\n"; }
 
    A(A&& o) : s(std::move(o.s)) {}
 
    A& operator=(const A& other)
    {
         s = other.s;
         std::cout << "复制赋值\n";
         return *this;
    }
 
    A& operator=(A&& other)
    {
         s = std::move(other.s);
         std::cout << "移动赋值\n";
         return *this;
    }
};
 
A f(A a) { return a; }
 
struct B : A
{
    std::string s2; 
    int n;
    // 隐式移动赋值运算符 B& B::operator=(B&&)
    // 调用 A 的移动赋值运算符
    // 调用 s2 的移动赋值运算符
    // 并进行 n 的逐位复制
};
 
struct C : B
{
    ~C() {} // 析构函数阻止隐式移动赋值
};
 
struct D : B
{
    D() {}
    ~D() {} // 析构函数本会阻止隐式移动赋值
    D& operator=(D&&) = default; // 无论如何都强制移动赋值
};
 
int main()
{
    A a1, a2;
    std::cout << "尝试从右值临时量移动赋值 A\n";
    a1 = f(A()); // 从右值临时量移动赋值
    std::cout << "尝试从亡值移动赋值 A\n";
    a2 = std::move(a1); // 从亡值移动赋值
 
    std::cout << "\n尝试移动赋值 B\n";
    B b1, b2;
    std::cout << "移动前,b1.s = \"" << b1.s << "\"\n";
    b2 = std::move(b1); // 调用隐式移动赋值
    std::cout << "移动后,b1.s = \"" << b1.s << "\"\n";
 
    std::cout << "\n尝试移动赋值 C\n";
    C c1, c2;
    c2 = std::move(c1); // 调用复制赋值运算符
 
    std::cout << "\n尝试移动赋值 D\n";
    D d1, d2;
    d2 = std::move(d1);
}

输出:

尝试从右值临时量移动赋值 A
移动赋值
尝试从亡值移动赋值 A
移动赋值
 
尝试移动赋值 B
移动前,b1.s = "测试"
移动赋值
移动后,b1.s = "" 
 
尝试移动赋值 C
复制赋值
 
尝试移动赋值 D
移动赋值

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1402 C++11 会调用非平凡复制赋值运算符的预置移动赋值运算符被弃置;
被弃置的预置移动赋值运算符仍参与重载决议
允许调用这种复制赋值运算符;
使重载决议将忽略它
CWG 1806 C++11 涉及虚基类的预置移动赋值运算符的规定缺失 已添加
CWG 2094 C++11 volatile 子对象使预置的移动赋值运算符非平凡(CWG 问题 496 平凡性不受影响
CWG 2180 C++11 T 的预置的复制赋值运算符在 T 是抽象类且拥有
无法被复制赋值的直接虚基类时不会被定义为弃置
此时会被定义为弃置
CWG 2690 C++11 隐式定义的联合体类型的移动赋值运算符不会复制对象表示 会复制对象表示

参阅