成员初值列的效率优势

以下两种构造函数有何区别?

class A{
public:
    A(int _a, int _b)
    :a(_a),
    b(_b)
    {
        // nothing to do
    }
private:
    int a;
    int b;
};
class B{
public:
    B(int _a, int _b)
    {
        a = _a;
        b = _b;
    }
private:
    int a;
    int b;
};

这两种成员变量初始化方式都非常常见,一些人(比如我)对左边的成员初值列写法具有天然的抗拒性,总是隐约的认为右边直接在函数中赋值的写法“可读性更高”(实际上只是因为自己更熟悉),而且不会带来任何效率损失。

实际上在上述代码中,这两种写法的编译结果是完全一致的,这也意味着两种写法没有任何效率上的差别。但在一些情况下,两种写法的确有着不容忽视的效率差别。

阅读以下代码:

class A{
public:
    A(int _a, int _b)
    :a(_a),
    b(_b)
    {
        // do nothing
    }
private:
    int a = 1;
    int b = 2;
};
class B{
public:
    B(int _a, int _b)
    {
        a = _a;
        b = _b;
    }
private:
    int a = 1;
    int b = 2;
};

当我们对成员变量定义了默认初始化时,两者在效率上的差别又如何呢?

观察 -O0 参数下的编译结果会发现,成员初值列的效率的确更高一些:

A::A(int, int) [base object constructor]:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     DWORD PTR [rbp-16], edx
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, DWORD PTR [rbp-12]
        mov     DWORD PTR [rax], edx
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, DWORD PTR [rbp-16]
        mov     DWORD PTR [rax+4], edx
        nop
        pop     rbp
        ret
B::B(int, int) [base object constructor]:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     DWORD PTR [rbp-16], edx
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], 1
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+4], 2
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, DWORD PTR [rbp-12]
        mov     DWORD PTR [rax], edx
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, DWORD PTR [rbp-16]
        mov     DWORD PTR [rax+4], edx
        nop
        pop     rbp
        ret

可以观察到,成员初值列跳过了默认初始化,进而直接用构造函数的参数对成员变量进行初始化;而函数中赋值则仍然使用了成员变量的默认初始化,随后其初始化结果被丢弃,进而使用参数对成员变量进行赋值。

实际上上述情况由于写法不太规范,也比较少见。一种比较常见的情况是某个类声明了一个结构体或是类成员变量,而它们自己定义了默认构造。但为了”确保每一个构造函数都将对象的每一个成员初始化“,我们又需要在构造函数中对其进行初始化,这时使用成员初值列的优势就非常明显了。

class A{
public:
    A(T _t)
    :t(_t)
    {
        // do nothing
    }
private:
    T t; // T is a struct or class type
}

成员初值列的写法没有调用T(),而是直接使用了自动生成的拷贝构造(不推荐使用自动生成的任何构造,以免造成难以预测的不良影响);

而相对的,在函数中赋值的写法在进入函数前先调用了T()对t进行初始化,然后将参数_t拷贝给成员变量t。

两种写法结果相同,但效率上差了一个T的构造,当T的结构非常复杂时,这个差别就会非常明显;如果进一步再去纠结函数跳转和入栈出栈,这种直接在函数中赋值的写法对效率的浪费程度简直让人难以忍受。

结论

成员初值列会用拷贝构造替代成员的默认构造,而函数中赋值的方式会毫无意义的调用默认构造,再对成员变量进行赋值。

为了确保每一个构造函数都将对象的每一个成员初始化,同时为了不浪费效率,应当尽量使用成员初值列的方式来初始化成员变量