移动赋值运算符

< cpp‎ | language

T 的移动赋值运算符是名为 operator=的非模板非静态成员函数,它接受恰好一个 T&&const T&&volatile T&&const volatile T&& 类型的形参。

语法

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

解释

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

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

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

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

若不对类类型(structclassunion)提供任何用户定义的移动赋值运算符,且下列各项均为真:

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

则编译器将声明一个移动赋值运算符,作为其类的 inline public 成员,并拥有签名 T& T::operator=(T&&)

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

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

因为始终对任何类声明某个赋值运算符(移动或复制),故基类的赋值运算符始终被隐藏。当使用 using 声明从基类带入赋值运算符,且其实参类型与派生类的隐式赋值运算符的实参类型相同时,该 using 声明亦为隐式声明所隐藏。

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

若下列任何一项为真,则类 T 的隐式声明或预置的移动赋值运算符被定义为弃置的

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

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

平凡移动赋值运算符

当下列各项均为真时,类 T 的移动赋值运算符为平凡的:

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

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

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

若隐式声明的移动赋值运算符既未被弃置亦非平凡,则当它被 ODR 式使用时,它为编译器所定义(即生成并编译函数体)。

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

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

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

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("test") { }
    A(const A& o) : s(o.s) { std::cout << "move failed!\n"; }
    A(A&& o) : s(std::move(o.s)) { }
    A& operator=(const A& other)
    {
         s = other.s;
         std::cout << "copy assigned\n";
         return *this;
    }
    A& operator=(A&& other)
    {
         s = std::move(other.s);
         std::cout << "move assigned\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 << "Trying to move-assign A from rvalue temporary\n";
    a1 = f(A()); // 从右值临时量移动赋值
    std::cout << "Trying to move-assign A from xvalue\n";
    a2 = std::move(a1); // 从亡值移动赋值
 
    std::cout << "Trying to move-assign B\n";
    B b1, b2;
    std::cout << "Before move, b1.s = \"" << b1.s << "\"\n";
    b2 = std::move(b1); // 调用隐式移动赋值
    std::cout << "After move, b1.s = \"" << b1.s << "\"\n";
 
    std::cout << "Trying to move-assign C\n";
    C c1, c2;
    c2 = std::move(c1); // 调用复制赋值运算符
 
    std::cout << "Trying to move-assign D\n";
    D d1, d2;
    d2 = std::move(d1);
}

输出:

Trying to move-assign A from rvalue temporary
move assigned
Trying to move-assign A from xvalue
move assigned
Trying to move-assign B
Before move, b1.s = "test"
move assigned
After move, b1.s = "" 
Trying to move-assign C
copy assigned
Trying to move-assign D
move assigned

缺陷报告

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

DR 应用于 出版时的行为 正确行为
CWG 1402 C++11 本会调用非平凡复制赋值运算符的预置移动赋值运算符被弃置;
被弃置的预置移动赋值运算符仍参与重载决议
允许调用这种复制赋值运算符;
使之在重载决议中被忽略
CWG 1806 C++11 涉及虚基类的预置移动赋值运算符的规定缺失 已添加
CWG 2094 C++11 volatile 子对象丢弃预置的移动赋值运算符的平凡性(CWG 496) 平凡性不受影响

参阅