28 September 2022

C++ 核心指南目录

C.43: Ensure that a copyable class has a default constructor

理由

可拷贝的类有默认构造函数就能确保:如果一个具体类是可拷贝的,它也就满足其他的“半正则”条件。

很多语言和库工具依赖默认构造函数初始化其元素。比如T a[10]std::vector<T> v(10) 。如果类是可拷贝的,有了默认构造函数,就能简化数据的移动操作。

例子

class Date { // BAD: no default constructor
public:
    Date(int dd, int mm, int yyyy);
    // ...
};

vector<Date> vd1(1000);   // default Date needed here
vector<Date> vd2(1000, Date{Month::October, 7, 1885});   // alternative

没有用户自定义的构造函数的时候,编译器才会自动生成一个默认构造函数。所以上面代码中,vector vd1是没法被初始化的,因为 Date 中已经定义了一个构造函数,编译器就不会生成默认的构造函数。没有默认构造函数,代码写起来容易变得复杂。如果可以,尽量使用默认构造函数。

对于日期类来说,定义一个顺其自然的默认日期比较困难。宇宙大爆炸对于大部分人来说太久远了。而 0 年 0 月 0 日,在大部分的日历系统中又是不存在的。这就好像把浮点数设置成 NaN 一样。

不过,大部分的日期类都有某个“起始日期”。比如最流行的 1970年 1 月1日。所以把这个日期作为默认日期就很容易了。

class Date {
public:
    Date(int dd, int mm, int yyyy);
    Date() = default; // [See also](#Rc-default)
    // ...
private:
    int dd = 1;
    int mm = 1;
    int yyyy = 1970;
    // ...
};

vector<Date> vd1(1000);

注意

一个类里所有的成员都有默认构造函数,这个类也隐式地获得了一个构造函数。

struct X {
    string s;
    vector<int> v;
};

X x; // means the empty string and the empty vector

注意,内置类型没有默认构造函数,其默认值可能是个随机数。

#include <iostream>
#include <string>
using namespace std;
struct X {
    string s;
    int i;
};

void f()
{
    X x;    // x.s is initialized to the empty string; x.i is uninitialized

    cout << x.s << ' ' << x.i << '\n';
    ++x.i;
}
int main()
{
    f();
}
-901376320

以上代码中,x.s初始化为空字符串。而x.i则是未初始化的一个数值。

静态分配的内置类型对象的默认值是 0,而局部的内置变量则不会初始化。注意,你的编译器可能会给局部内置变量设定一个初始值。但是当你进行优化编译的时候,就不设置了。所以,以上代码看起来可以工作,是基于未定义的行为,所以并不可靠。假设你需要进行初始化,那么请明确定义初始化过程。

#include <iostream>
#include <string>
using namespace std;
struct X {
    string s;
    int i {};   // default initialize (to 0)
};
void f()
{
    X x;    // x.s is initialized to the empty string; x.i is uninitialized

    cout << x.s << ' ' << x.i << '\n';
    ++x.i;
}
int main()
{
    f();

}
0

注意

没有合理的默认构造函数的类,通常也不能拷贝,因此,这些类不在本指南建议范围内。

比如,一个基类如果不能拷贝的话,那么就不需要一个默认构造函数。

// Shape is an abstract base class, not a copyable type.
// It might or might not need a default constructor.
struct Shape {
    virtual void draw() = 0;
    virtual void rotate(int) = 0;
    // =delete copy/move functions
    // ...
};

一个类如果在构建的过程中,需要接受一个调用者提供的资源的话,一般不能有默认构造函数。这种类,不在本指南建议范围,因为这些类通常不可拷贝。

// std::lock_guard is not a copyable type.
// It does not have a default constructor.
lock_guard g {mx};  // guard the mutex mx
lock_guard g2;      // error: guarding nothing

一个类如果有某个“特殊状态”,就必须单独由某个成员函数,或由额外过程来处理状态变化。不管是否能拷贝,这种类型很自然的会用某个特殊状态作为默认构造值。

// std::ofstream is not a copyable type.
// It does happen to have a default constructor
// that goes along with a special "not open" state.
ofstream out {"Foobar"};
// ...
out << log(time, transaction);

类似的,特殊状态类型是可拷贝的。比如可拷贝的智能指针就有个特殊状态 “==nullptr”,必须用这个特殊状态作为其默认构造值。

不管怎么样,最好有一个默认构造函数,将类的默认值初始化为一些有意义的状态,如std::string""std::vector{}

强化

  • 标注用=的可拷贝类,却没有默认构造函数
  • 标注用==可比较的类,却不能拷贝