复制赋值运算符

来自cppreference.com
< 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=(T&)T& T::operator=(T)当存在用户定义的复制赋值运算符时,用户仍然可以通过关键词 default 强迫编译器生成隐式声明的复制赋值运算符。 (C++11 起)

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

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

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

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

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

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

如果满足下列任一条件,那么类 T 的预置的复制赋值运算符被定义为弃置的

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

平凡的复制赋值运算符

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

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

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

合格的复制赋值运算符

被用户声明或者同时被隐式声明且可定义的复制赋值运算符是合格的。

(C++11 前)

没有被弃置的复制赋值运算符是合格的。

(C++11 起)
(C++20 前)

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

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

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

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

如果隐式声明的复制赋值运算符既没有被弃置也不平凡,那么当它被 ODR 式使用或用于常量求值 (C++14 起)时,它会被编译器定义(即生成并编译函数体)。对于联合体类型,隐式定义的复制赋值运算符(如同以 std::memmove)复制其对象表示。对于非联合类类型(classstruct),编译器按照声明顺序对对象的各基类和非静态成员进行逐成员复制赋值,其中对标量进行内建赋值,而对类类型使用复制赋值运算符,

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

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

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 << "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 (this != &other) // 非自赋值
        {
            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 调用 ";
    a1 = a2; // 用户定义的复制赋值
 
    B b1, b2;
    b2.s1 = "foo";
    b2.s2 = "bar";
    std::cout << "b1 = b2 调用 ";
    b1 = b2; // 隐式定义的复制赋值
    std::cout << "b1.s1 = " << b1.s1 << " b1.s2 = " << b1.s2 << '\n';
}

输出:

a1 = a2 调用 A 的复制赋值
b1 = b2 调用 A 的复制赋值
b1.s1 = foo b1.s2 = bar

缺陷报告

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

DR 应用于 出版时的行为 正确行为
CWG 2094 C++11 volatile 子对象使得预置的默认复制赋值运算符非平凡(CWG 496) 平凡性不受影响
CWG 2171 C++11 operator=(X&) = default 非平凡 令它平凡

参阅