复制赋值运算符

< cpp‎ | language

T 的复制赋值运算符是名为 operator= 的非模板非静态成员函数,它接收恰好一个 TT&const T&volatile T&const volatile T& 类型的形参。对于可复制赋值 (CopyAssignable) 类型,它必须有公开的复制赋值运算符。

语法

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

解释

  1. 当可以使用复制并交换手法时,复制赋值运算符的典型声明。
  2. 当不能使用复制并交换手法时(不可交换类型或有性能退化),复制赋值运算符的典型声明。
  3. 强制编译器生成复制赋值运算符。
  4. 避免隐式复制赋值。

凡在为重载决议所选择时,复制赋值运算符得到调用,例如对象出现在赋值表达式左侧时。

隐式声明的复制赋值运算符

若不对类类型(structclassunion)提供任何用户定义的复制赋值运算符,则编译器将始终声明一个,作为类的 inline public 成员。当以下各项均为真时,这个隐式声明的复制赋值运算符拥有形式 T& T::operator=(const T&)

  • T 的每个直接基类 B 均拥有复制赋值运算符,其形参是 Bconst B&const volatile B&
  • T 的每个类类型或类数组类型的非静态数据成员 M 均拥有复制赋值运算符,其形参是 Mconst M&const volatile M&

否则隐式声明的复制赋值运算符被声明为 T& T::operator=(T&)。(注意因为这些规则,隐式声明的复制赋值运算符不能绑定到 volatile 左值实参。)

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

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

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

弃置的隐式声明的复制赋值运算符

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

  • T 拥有用户声明的移动构造函数;
  • T 拥有用户声明的移动赋值运算符。

否则,它被定义为预置的。

若下列任何一项为真,则类 T 的预置的复制赋值运算符被定义为弃置的

  • T 拥有非类类型(或其数组)的 const 限定的非静态数据成员;
  • T 拥有引用类型的非静态数据成员;
  • T 拥有无法复制赋值的非静态数据成员,或直接或虚基类(对复制赋值的重载决议失败,或选择弃置或不可访问的函数);
  • T联合体式的类,且拥有对应复制赋值运算符非平凡的变体成员。

平凡复制赋值运算符

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

  • 它不是用户提供的(即它是隐式定义或预置的),且若它被预置,则其签名与隐式定义的相同 (C++14 前)
  • T 没有虚成员函数;
  • T 没有虚基类;
  • T 的每个直接基类选择的复制赋值运算符都是平凡的;
  • T 的每个类类型(或类类型的数组)的非静态数据成员选择的复制赋值运算符都是平凡的;
  • T 没有 volatile 限定类型的非静态数据成员。
(C++14 起)

平凡复制赋值运算符如同用 std::memmove 进行对象表示的复制。所有与 C 语言兼容的数据类型(POD 类型)都是可平凡复制的。

隐式定义的复制赋值运算符

若隐式声明的复制赋值运算符既未被弃置亦非平凡,则当它被 ODR 式使用时,它为编译器所定义(即生成并编译函数体)。对于 union 类型,隐式定义的复制赋值运算符(如用 std::memmove)复制其对象表示。对于非联合类类型(classstruct),编译器用标量的内建赋值和类类型的复制赋值运算符,以其声明顺序,对对象的各基类和非静态成员进行逐成员复制赋值。

T 拥有用户定义的析构函数或用户定义的复制赋值运算符时,隐式定义的复制赋值运算符的生成被弃用。(C++11 起)

注解

若一同提供了复制与移动赋值运算符,则当实参为右值(如无名临时量的纯右值,或如 std::move 的结果的亡值)时,重载决议选择移动赋值,当实参为左值(具名对象或返回左值引用的函数/运算符)时,选择复制赋值。若仅提供了复制赋值,则所有值类别时都选择它(只要它按值或按到 const 的引用接收其实参),这使得在移动不可用时,以复制赋值成为移动赋值的后备。

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

有关用户定义的复制赋值运算符的受期待行为的额外细节,见赋值运算符重载

示例

#include <iostream>
#include <memory>
#include <string>
#include <algorithm>
 
struct A
{
    int n;
    std::string s1;
    // 用户定义的复制赋值,复制并交换形式
    A& operator=(A other)
    {
        std::cout << "copy assignment of A\n";
        std::swap(n, other.n);
        std::swap(s1, other.s1);
        return *this;
    }
};
 
struct B : A
{
    std::string s2;
    // 隐式定义的复制赋值
};
 
struct C
{
    std::unique_ptr<int[]> data;
    std::size_t size;
    // 非复制并交换的赋值
    C& operator=(const C& other)
    {
        // 检查自赋值
        if(&other == this)
            return *this;
        // 可能时复用存储
        if(size != other.size)
        {
            data.reset(new int[other.size]);
            size = other.size;
        }
        std::copy(&other.data[0], &other.data[0] + size, &data[0]);
        return *this;
    }
    // 注意:复制并交换始终导致重分配
};
 
int main()
{
    A a1, a2;
    std::cout << "a1 = a2 calls ";
    a1 = a2; // 用户定义的复制赋值
 
    B b1, b2;
    b2.s1 = "foo";
    b2.s2 = "bar";
    std::cout << "b1 = b2 calls ";
    b1 = b2; // 隐式定义的复制赋值
    std::cout << "b1.s1 = " << b1.s1 << " b1.s2 = " << b1.s2 << '\n';
}

输出:

a1 = a2 calls copy assignment of A
b1 = b2 calls copy assignment of A
b1.s1 = foo b1.s2 = bar

缺陷报告

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

DR 应用于 出版时的行为 正确行为
CWG 2171 C++14 operator=(X&) = default 非平凡 令它平凡