前言
这是一个非常常见又有趣但真正知道的人又很少的问题。 首先第一个问题,什么是成员初始化器?
在类的构造函数定义中,成员初始化器列表指定各个直接基类、虚基类和非静态数据成员的初始化器。
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)
}
语法形式还是比较简单,好的,到此我们明确了第一个问题,什么是成员初始化器。
为什么我们要优先使用成员初始化器?
既然优先使用这个东西,那么它必然可以带来一些好处,它能带来什么好处呢?
这是多方面的,维护、可读性、效率。
- 当你在类中添加新的成员时,你只需要在成员初始化器中添加初始化,而不必修改构造函数体。
- 构造函数可能会做很多事情,不单单是初始化类的成员。使用成员初始化器,可以将这些责任划分出去,构造函数的代码也会减少和更加直观。初始化的事情给了成员初始化器,成员初始化器只能用于初始化(除了委托构造),也更加明确。
- 也可能是最受关注的,效率问题!
效率问题我们先用一个简单的 demo 展示。
#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);
}
}
X()
X& operator=(int)
-------
X(int)
我相信以上的 demo 很直观:使用成员初始化器是直接调用对应的构造函数构造出对象,而且更加的直观;写在构造函数再进行初始化有着额外的开销,而且可读性不高,并且其实你没办法保证这些类都有 =
(这个 =
指代的是所有的给对象初始化的方式)。
我们举例一个复杂和实际一点的例子,如下:
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 的实现则是有些奇怪,如果不以成员初始化,有不少明显的额外开销,比如下面这种形式:
#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 就没这个问题。
谁知道它咋实现的呢。
和类内默认初始化一起使用
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 };
};
- 在设计新类时遵循的方法是,在类的主体中定义默认行为。明确定义的构造函数只用来改变默认行为。
如果不这样做,会更加难以理解,类也更难维护。
当你在类中添加新成员时,你只需要在类的主体中添加初始化,而不必在所有的构造函数中添加。此外,你也不需要考虑将初始化器按正确的顺序放在构造函数中了。
下面是错误示范:
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;
};
总结
别想那么多,尽量用成员初始化器即可。