Skip to content

前言

这是一个非常常见又有趣但真正知道的人又很少的问题。 首先第一个问题,什么是成员初始化器

在类的构造函数定义中,成员初始化器列表指定各个直接基类、虚基类和非静态数据成员的初始化器。

cpp
struct S{
    int n;
 
    S(int);       // 构造函数声明
 
    S() : n(7) {} // 构造函数定义:
                  // ": n(7)" 是初始化器列表
                  // ": n(7) {}" 是函数体
};
 
S::S(int x) : n{x} {} // 构造函数定义:": n{x}" 是初始化器列表
 
int main(){
    S s;      // 调用 S::S()
    S s2(10); // 调用 S::S(int)
}

语法形式还是比较简单,好的,到此我们明确了第一个问题,什么是成员初始化器。

为什么我们要优先使用成员初始化器?

既然优先使用这个东西,那么它必然可以带来一些好处,它能带来什么好处呢?

这是多方面的,维护、可读性、效率

  1. 当你在类中添加新的成员时,你只需要在成员初始化器中添加初始化,而不必修改构造函数体。
  2. 构造函数可能会做很多事情,不单单是初始化类的成员。使用成员初始化器,可以将这些责任划分出去,构造函数的代码也会减少和更加直观。初始化的事情给了成员初始化器,成员初始化器只能用于初始化(除了委托构造),也更加明确。
  3. 也可能是最受关注的,效率问题!

效率问题我们先用一个简单的 demo 展示。

cpp
#include <iostream>

struct X{
    X() { puts("X()"); }
    X(int) { puts("X(int)"); }
    X& operator=(int){
        puts("X& operator=(int)");
        return *this;
    }
};

struct Y{
    X x;
    Y(int v){
        x = v;
    }
};
struct Y2{
    X x;
    Y2(int v) :x{ v } {}
};

int main(){
    {
        Y y(1);
    }
    puts("-------");
    {
        Y2 y(1);
    }
}

运行结果

txt
X()
X& operator=(int)
-------
X(int)

我相信以上的 demo 很直观:使用成员初始化器是直接调用对应的构造函数构造出对象,而且更加的直观;写在构造函数再进行初始化有着额外的开销,而且可读性不高,并且其实你没办法保证这些类都有 =(这个 = 指代的是所有的给对象初始化的方式)。

我们举例一个复杂和实际一点的例子,如下:

cpp
struct scope_guard {
    std::function<void()> func;
    template<typename F, typename...Args>
    scope_guard(F&& f, Args&&... args) :func(std::bind(std::forward<decltype(f)>(f), std::forward<decltype(args)>(args)...)) {}
    ~scope_guard() { func(); }
    scope_guard(const scope_guard&) = delete;
    scope_guard& operator=(const scope_guard&) = delete;
};

scope_guard 类型的作用非常的简单,如你所见,构造函数根据传入的可调用对象和参数,把他们包装起来,使用 std::bind 初始化数据成员 std::function<void()> func ,存起来,到析构的时候再进行调用,也就是一个很普通的 RAII 的形式。

事实上实测并没有多大区别,不过测的都是 libstdc++ 和 libc++,msvc 的 std::function 的实现则是有些奇怪,如果不以成员初始化,有不少明显的额外开销,比如下面这种形式:

cpp
#include <iostream>
#include <functional>

struct X {
    X() { puts("X()"); }
    X(const X&) { puts("X(const X&)"); }
    X(X&&)noexcept { puts("X(X&&)"); }
    ~X() { puts("~X()"); }
};

struct scope_guard {
    std::function<void()> func;
    template<typename F, typename...Args>
    scope_guard(F&& f, Args&&... args) {
        func = std::bind(std::forward<decltype(f)>(f), std::forward<decltype(args)>(args)...);
    }
    ~scope_guard() { func(); }
    scope_guard(const scope_guard&) = delete;
    scope_guard& operator=(const scope_guard&) = delete;
};

void f(X) {}

int main() {
    X x;
    scope_guard s{ f,x };
}

你可以看到,我们这段代码的 scope_guard 没有使用成员初始化器,在 msvc 下运行之后,相比使用了成员初始化器,多了两个移动构造的开销,gcc 和 clang 就没这个问题

谁知道它咋实现的呢。

和类内默认初始化一起使用

cpp
class WidgetImpro {
public:
    WidgetImpro() = default;
    explicit WidgetImpro(int w) :width(w), height(getHeight(w)) {}
    WidgetImpro(int w, int h) :width(w), height(h) {}

    void show()const{
        std::cout << std::boolalpha << width << "x" << height
            << ", frame: " << frame
            << ", visible: " << visible << '\n';
    }
private:
    int getHeight(int w) { return w * 3 / 4; }
    int width{ 640 };
    int height{ 480 };
    bool frame{ false };
    bool visible{ true };
};
  • 在设计新类时遵循的方法是,在类的主体中定义默认行为。明确定义的构造函数只用来改变默认行为。

如果不这样做,会更加难以理解,类也更难维护。

当你在类中添加新成员时,你只需要在类的主体中添加初始化,而不必在所有的构造函数中添加。此外,你也不需要考虑将初始化器按正确的顺序放在构造函数中了。

下面是错误示范:

cpp
class Widget {
public:
    Widget() :width(640), height(480), frame(false), visible(true) {}
    explicit Widget(int w) :width(w), height(getHeight(w)), frame(false), visible(true) {}
    Widget(int w, int h) :width(w), height(h), frame(false), visible(true) {}

    void show()const {
        std::cout << std::boolalpha << width << "x" << height
            << ", frame: " << frame
            << ", visible: " << visible << '\n';
    }
private:
    int getHeight(int w) { return w * 3 / 4; }
    int width;
    int height;
    bool frame;
    bool visible;
};

总结

别想那么多,尽量用成员初始化器即可。