1.如果你需要明确使用默认语义,则使用=default.
如果你定义了五个特殊成员函数中的任何之一,你就必须把它们全都定义了一下。这五个特殊成员函数是除默认构造函数外的其他所有特殊成员函数。
当定义析构函数时,必须定义拷贝和移动构造函数以及拷贝和移动赋值运算符。最简单的方法是通过=default来请求其余四个特殊成员函数。
class Tracer
{
std::string message;
public:
explicit Tracer(const std::string& m) : message{m}
{
std::cerr << "entering" << message << '\n';
}
~Tracer()
{
std::cerr << "exiting" << message << '\n';
}
Tracer(const Tracer&) = default;
Tracer& operator = (const Tracer&) = default;
Tracer(Tracer&&) = default;
Tracer& operator = (Tracer&&) = default;
};
2.当想要禁用默认行为(且不需要替代方法)时使用=delete
有时,你想禁用默认操作。这时候,delete就发挥作用了。C++做到了“吃自家的狗粮”。几乎所有来自线程API的类型的拷贝构造函数都被设置为delete。mutex(互斥量)、lock(锁)或feature(期值)等数据类型都是如此。
可以使用delete来创建奇怪的类型,下面Immortal的实例不能被析构。
class Immortal
{
public:
~Immortal() = delete; //不允许析构
};
int main()
{
Immortal im; //(1)
Immortal* pIm = new Immortal;
delete pIm; //(2)
}
对析构函数(1)的隐式调用或对析构函数 (2)的显式调用都会导致编译器错误。
3.不要在构造函数和析构函数中调用虚函数
从构造函数或析构函数中调用纯虚函数是未定义行为。从构造函数或析构函数中调用虚函数的行为不会像你期望的那样起作用。出于保护的原因,虚调用机制在构造函数或析构函数中被禁用,因而你会得到非虚的调用。
struct Base
{
Base()
{
f();
}
virtual void f()
{
std::cout << "Base called" << "\n";
}
};
struct Derived : Base
{
void f() override
{
std::cout << "Derived called" << "\n";
}
};
int main()
{
Derived d; //Base called
}
swap函数
一个类型要成为规范类型的话,就必须支持swap函数。规范类型的一个不那么正式的叫法是“类值”类型。
4.对于类值类型,考虑提供一个noexcept的交换函数
class Foo
{
public:
void swap(Foo& rhs) noexcept
{
m1.swap(rhs.m1);
std::swap(m2, rhs.m2);
}
private:
Bar m1;
int m2;
};
为了方便起见,应考虑在已实现的swap成员函数的基础上支持非成员的swap函数。
void swap(Foo& a, Foo& b) noexcept
{
a.swap(b);
}
如果你不提供非成员的swap函数,那么需要交换的标准库算法(如std::sort和std::rotate)将退回到std::swap模板,它是用移动构造和移动赋值来定义的。
template<typename T>
void std::swap(T& a, T& b) noexcept
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
拷贝并交换惯用法
如果你使用拷贝并交换惯用法来实现拷贝赋值和移动赋值运算法,就必须定义你自己的swap函数——作为成员函数或者作为友元函数。我在Cont类中添加了swap函数,并在拷贝赋值和移动赋值运算符中进行使用。
class Cont
{
public:
Cont() = default;
Cont (const Cont& rhs) = default;
Cont(Cont&& rhs) = default;
Cont& operator = (const Cont& rhs);
Cont& operator = (Cont&& rhs);
friend void swap(Cont& lhs, Cont& rhs) noexcept
{
std::swap(lhs.size, rhs.size);
std::swap(lhs.pData, rhs.pData);
}
private:
int* pData;
std::size_t size;
};
Cont& Cont::operator=(const Cont& rhs)
{
Cont tmp(rhs);
swap(*this, tmp);
return *this;
}
Cont& Cont::operator=(Cont&& rhs)
{
Cont tmp(std::move(rhs));
swap(*this, tmp);
return *this;
}
当swap函数基于拷贝语义而不是移动语义时,它可能会因为内存耗尽而失败。
C++98里的swap实现:
template<typename T>
void std::swap(T& a, T& b) noexcept
{
T tmp = a;
a = b;
b = tmp;
}
在这种情况下,内存耗尽会导致std::bad_alloc异常。
相等运算符
要成为规范类型,数据类型还必须支持相等运算符。
5.使==对操作数的类型对称,并使其noexcept
下面的代码片段显示了一个不直观的相等运算符,它被定义在类内部。
class MyInt
{
int num;
public:
MyInt(int n) : num(n) {};
bool operator == (const MyInt& rhs) const noexcept
{
return num == rhs.num;
}
};
int main()
{
MyInt(5) == 5; // 可以
5 == MyInt(5); //出错
}
调用MyInt(5) == 5是有效的,因为构造函数将int转换为MyInt的实例,最后一行(5 == MyInt(5))会报错,一个int类型的对象不能与一个MyInt对象进行比较,也不存在可以从MyInt到int的转换。
解决这种不对称性的优雅方法是在MyInt类中声明一个友元函数 ==,下面是MyInt的改进版本。
class MyInt
{
int num;
public:
MyInt(int n) : num(n) {};
friend bool operator == (const MyInt& lhs, const MyInt& rhs) noexcept
{
return lhs.num == rhs.num;
}
};
int main()
{
MyInt(5) == 5; // 可以
5 == MyInt(5); //可以
}
class MyInt
{
int num;
public:
explicit MyInt(int n) : num(n) {};
friend bool operator == (const MyInt& lhs, const MyInt& rhs) noexcept
{
return lhs.num == rhs.num;
}
};
int main()
{
MyInt(5) == 5; // 出错
5 == MyInt(5); // 出错
}
若将构造函数声明为explicit,会破坏从int到MyInt的隐式转换。可通过提供两个额外的重载来解决这个问题。其中一个重载将int作为左参数,另一个将int作为有参数。
class MyInt
{
int num;
public:
explicit MyInt(int n) : num(n) {};
friend bool operator == (const MyInt& lhs, const MyInt& rhs) noexcept
{
return lhs.num == rhs.num;
}
friend bool operator == (int lhs, const MyInt& rhs) noexcept
{
return lhs == rhs.num;
}
friend bool operator == (const MyInt& lhs, int rhs) noexcept
{
return lhs.num == rhs;
}
};
int main()
{
MyInt(5) == 5; // 可以
5 == MyInt(5); //可以
}
6.当心基类上的==
struct BaseTest
{
std::string name;
int number;
virtual bool operator == (const BaseTest& a) const
{
return name == a.name && number == a.number;
}
};
struct DerivedTest : public BaseTest
{
char character;
virtual bool operator == (const DerivedTest& a) const
{
return name == a.name && number == a.number && character == a.character;
}
};
int main()
{
BaseTest b;
BaseTest& base = b;
DerivedTest d;
DerivedTest& derived = d;
base == derived; //比较name和number,但忽略了character
derived == base; //错误,没有定义==
DerivedTest derived2;
derived == derived2; //比较name和number,但忽略了character
BaseTest& base2 = derived2;
base2 == derived; //比较name和number,但忽略了character
return 0;
}
比较BaseTest的实例或DerivedTest的实例是可行的。但是,混合BaseTest和DerivedTest的实例并不像预期的那样工作。使用Base的==运算符忽略了DerivedTest里的character。使用DerivedTest的运算符对BaseTest的实例不起作用(编译错误),最后一行相当棘手,它使用了BaseTest的相等运算符,为什么?DerivedTest的==运算符应该覆盖了BaseTest的==运算符。不是这样!这两个运算符的签名不同。一个运算符需要BaseTest的一个实例,另一个运算符需要DerivedTest的一个实例。DerivedTest版本并没有覆盖BaseTest的版本。
这些结果也适用于其他五个比较运算符:!=, <, <=, > 和 >=。这种错误行为是切片问题的另一个方面——“多态类应当抑制公开的拷贝/移动操作”。