特殊成员函数的隐式合成与抑制机制¶
C++11 中的特殊成员函数多了两个:移动构造函数和移动赋值运算符。
class A {
public:
A(A&& rhs); //@ 移动构造函数
A& operator=(A&& rhs); //@ 移动赋值运算符
};
移动操作同样会在需要时生成,执行的是对 non-static 成员的移动操作,另外它们也会对基类部分执行移动操作。
移动操作并不确保真正移动,其核心是把 std::move 用于每个要移动的对象,根据返回值的重载解析决定执行移动还是拷贝。因此按成员移动分为两部分:
- 对支持移动操作的类型进行移动。
- 对不可移动的类型执行拷贝。
两种拷贝操作(拷贝构造函数和拷贝赋值运算符)是独立的,声明其中一个不会阻止编译器生成另一个。
两种移动操作是不独立的,声明其中一个将阻止编译器生成另一个。理由是如果声明了移动构造函数,可能意味着实现上与编译器默认按成员移动的移动构造函数有所不同,从而可以推断移动赋值操作也应该与默认行为不同。
显式声明拷贝操作(即使声明为 =delete)会阻止自动生成移动操作(但声明为 =default 不阻止生成)。理由类似上条,声明拷贝操作可能意味着默认的拷贝方式不适用,从而推断移动操作也应该会默认行为不同。
反之亦然,声明移动操作也会阻止生成拷贝操作。
C++11 规定,显式声明析构函数会阻止生成移动操作。这个规定源于 Rule of Three,即两种拷贝函数和析构函数应该一起声明。这个规则的推论是,如果声明了析构函数,则说明默认的拷贝操作也不适用,但 C++98 中没有重视这个推论,因此仍可以生成拷贝操作,而在 C++11 中为了保持不破坏遗留代码,保留了这个规则。由于析构函数和拷贝操作需要一起声明,加上声明了拷贝操作会阻止生成移动操作,于是 C++11 就有了这条规定。
最终,生成移动操作的条件必须满足:该类没有用户声明的拷贝、移动、析构中的任何一个函数。
总有一天这个规则会扩展到拷贝操作,因为 C++11 规定存在拷贝操作或析构函数时,仍能生成拷贝操作是被废弃的行为。C++11 提供了 =default 来表示使用默认行为,而不抑制生成其他函数。
这种手法对于多态基类很有用,多态基类一般会有虚析构函数,虚析构函数的默认实现一般是正确的,为了使用默认行为而不阻止生成移动操作,则应该使用 =default,同理,如果要使用默认的移动操作而不阻止生成拷贝操作,则应该给移动操作加上 =default。
class A {
public:
virtual ~A() = default;
A(A&&) = default; //@ support moving
A& operator=(A&&) = default;
A(const A&) = default; //@ support copying
A& operator=(const A&) = default;
};
事实上不需要思考太多限制,如果需要默认操作就使用 =default,虽然麻烦一些,但可以避免许多问题。
class StringTable {
public:
… //@ 实现插入、删除、查找等函数
private:
std::map<int, std::string> values;
};
上面的类没有声明任何特殊成员函数,编译器将在需要时自动合成。假设过了一段时间后,想扩充一些行为,比如记录构造和析构日志。
class StringTable {
public:
StringTable() { makeLogEntry("Creating StringTable object"); }
~StringTable() { makeLogEntry("Destroying StringTable object"); }
…
private:
std::map<int, std::string> values;
};
这时析构函数就会阻止生成移动操作,但针对移动操作的测试可以通过编译,因为在不可移动时会使用拷贝操作,而这很难被察觉。执行移动的代码实际变成了拷贝,而这一切只是源于添加了一个析构函数。避免这个问题也不是难事,只需要一开始把拷贝和移动操作声明为 =default。
另外还有默认构造函数和析构函数的生成未被提及,这里将统一总结:
- 默认构造函数:和 C++98 相同,只在类中不存在用户声明的构造函数时生成。
- 析构函数:
- 和 C++98 基本相同,唯一的区别是默认为
noexcept。 - 和 C++98 相同,只有基类的析构函数为虚函数,派生类的析构函数才为虚函数。
- 和 C++98 基本相同,唯一的区别是默认为
- 拷贝构造函数:
- 仅当类中不存在用户声明的拷贝构造函数时生成。
- 如果声明了移动操作,则拷贝构造函数被删除。
- 如果声明了拷贝赋值运算符或析构函数,仍能生成拷贝构造函数,但这是被废弃的行为。
- 拷贝赋值运算符:
- 仅当类中不存在用户声明的拷贝赋值运算符时生成。
- 如果声明了移动操作,则拷贝赋值运算符被删除。
- 如果声明了拷贝构造函数或析构函数,仍能生成拷贝赋值运算符,但这是被废弃的行为。
- 移动操作:仅当类中不存在任何用户声明的拷贝操作、移动操作、析构函数时生成。
注意,这些机制中提到的是成员函数而非成员函数模板,模板并不会影响特殊成员函数的合成。
class A {
public:
template<typename T>
A(const T& rhs); //@ 从任意类型构造
template<typename T>
A& operator=(const T& rhs); //@ 从任意类型赋值
…
};
上述模板不会阻止编译器生成拷贝和移动操作,即使模板的实例化和拷贝操作签名相同(即 T 是 A)。