复制赋值运算符

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

复制赋值运算符是名字为 operator= 的非模板非静态成员函数,它提供一个相同类类型实参就能调用,并且在不修改实参的情况下复制该实参的内容。

语法

关于复制赋值运算符的正式语法,可以参考函数声明。以下列出的语法只是合法复制赋值运算符语法的一部分。

返回类型 operator=(形参列表 ); (1)
返回类型 operator=(形参列表 ) 函数体 (2)
返回类型 operator=(无默认形参列表 ) = default; (3) (C++11 起)
返回类型 operator=(形参列表 ) = delete; (4) (C++11 起)
返回类型 类名 ::operator=(形参列表 ) 函数体 (5)
返回类型 类名 ::operator=(无默认形参列表 ) = default; (6) (C++11 起)
类名 - 要声明复制赋值运算符的类,它的类型在以下描述中给定为 T
形参列表 - 只有一个形参的形参列表,该形参的类型是 TT&const T&volatile T&const volatile T&
无默认形参列表 - 只有一个形参的形参列表,该形参的类型是 TT&const T&volatile T&const volatile T&,并且没有默认实参
函数体 - 复制赋值运算符的函数体
返回类型 - 任意类型,但为了允许链式赋值一般倾向于 T&

解释

1) 类定义中的复制赋值运算符的声明。
2-4) 类定义中的复制赋值运算符的定义。
3) 复制赋值运算符会被显式预置。
4) 复制赋值运算符会被弃置。
5,6) 类定义之外的复制赋值运算符的定义(该类必须包含一条声明 (1))。
6) 复制赋值运算符会被显式预置。
struct X
{
    X& operator=(X& other);     // 复制赋值运算符
    X operator=(X other);       // 允许按值传递
//  X operator=(const X other); // 错误:形参类型不正确
};
 
union Y
{
    // 复制赋值运算符并不一定要完全遵循上述列出的语法,
    // 它们只需要在不违反上述限制的情况下遵循通常函数声明的语法
    auto operator=(Y& other) -> Y&;       // OK:尾随返回类型
    Y& operator=(this Y& self, Y& other); // OK:显式对象形参
//  Y& operator=(Y&, int num = 1);        // 错误:有其他非对象形参
};

每当重载决议选择复制赋值运算符时,它都会被调用,例如对象出现在赋值表达式左侧时。

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

如果没有对类类型提供任何用户定义的复制赋值运算符,那么编译器将始终声明一个复制赋值运算符作为类的 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 声明也会被隐式声明所隐藏。

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

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

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

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

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

(C++23 起)

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

(C++11 起)

弃置的复制赋值运算符

如果满足以下任意条件,那么类 T 中隐式声明的或显式预置的 (C++11 起)复制赋值运算符不被定义 (C++11 前)被定义为弃置的 (C++11 起)

  • T 有一个具有 const 限定的非类类型(或它的可以有多维的数组)的非静态数据成员。
  • T 有一个具有引用类型的非静态数据成员。
  • T 有一个具有类类型 M(或它的可以有多维的数组类型)的潜在构造的子对象,并且为寻找 M 的复制赋值运算符而进行的重载决议
  • 没有得到可用候选,或者
  • 在该子对象是变体成员时,选择了非平凡的函数。

T 中隐式声明的复制赋值运算符在 T 声明了移动构造函数移动赋值运算符的情况下会被弃置。

(C++11 起)

平凡的复制赋值运算符

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

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

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

合格的复制赋值运算符

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

(C++11 前)

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

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

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

  • 它没有被弃置。
  • 它的所有关联约束(如果存在)都得到满足。
  • 在所有满足关联约束的复制赋值运算符中,它比其他所有复制赋值运算符都更受约束
(C++20 起)

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

注解

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

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

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

示例

#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
 
struct A
{
    int n;
    std::string s1;
 
    A() = default;
    A(A const&) = default;
 
    // 用户定义的复制赋值(复制交换法)
    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++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1353 C++98 不定义隐式声明的复制赋值运算符的条件没有考虑多维数组类型 考虑这些类型
CWG 2094 C++11 volatile 子对象使得预置的默认复制赋值运算符不平凡(CWG 问题 496 平凡性不受影响
CWG 2171 C++11 operator=(X&) = default 不平凡 令它平凡
CWG 2180 C++11 T 的预置的复制赋值运算符在 T 是抽象类且拥有
无法被复制赋值的直接虚基类时不会被定义为弃置
此时会被定义为弃置
CWG 2595 C++20 对于一个复制赋值运算符,如果存在其他更受约束但
无法满足关联约束的复制赋值运算符,那么它无法合格
此时它可以合格

参阅