问题引入

考虑下面的代码:

#include "X.h"
X foo()
{
    X xx;
    // ...
    return xx;
}

《对象模型》为此给出了两个命题:

  1. 每次foo()被调用,就返回xx的值。
  2. 如果X定义了一个copy constructor,当foo()被调用时,该constructor也会被调用。
    命题1的真假与X的定义有关,命题二的真假与X有关,但更取决于编译器的进取性优化层级(degree of aggressive optimization)。

    显示初始化操作

    已知:

    X x0;

    有三种方式,可以使用x0初始化X对象:

    void foo_bar()
    {
     X x1(x0);
     X x2 = x0;
     X x3 = X(x0);
    }

    foo_bar()可能被转化成:

    void foo_bar()
    {
     X x1;
     X x2;
     X x3;
     x1.X::X(x0);
     x2.X::X(x0);
     x3.X::X(x0);
    }

    参数初始化

    C++标准说,把一个对象当作参数传给一个函数,相当于进行了这样的初始化:X xx = arg;
    其中xx代表形参,arg代表真正的参数。如果foo()接受一个X类型的参数:void foo(X x0);,并且进行如下的调用:

    X xx;
    foo(xx);

    局部实例x0会把xx作为初值,以member-wise的方式进行初始化。具体到编译器的具体实现,有一种手法是使用临时的对象,通过copy constructor将它初始化,把它的引用作为参数传给函数。这使得上面的代码被转化为:

    X _tempX;
    _tempX.X::X(xx);
    foo(_tempX);
    void foo(X& x0);
    _tempX.X::~X();

    返回值的初始化

    已知函数定义:

    X bar()
    {
     X xx;
     // ...
     return xx;
    }

    bar()的返回值如何从局部对象xx中拷贝?cfront中的解决方式是双阶段转化:

    void bar(X& _result)
    {
     X xx;
     xx.X::X();
     // ...
     _result.X::X(xx);
     return;
    }

    这使得X xx = bar();看起来像是:

    X xx;
    bar(xx);

    如果X有成员函数X::memfunc(),则bar().memfunc()可能被转化为:

    X _tempX;
    (bar(_tempX), _tempX).memfunc();
    //书中这样写,有些疑惑

    同样的,如果有一个函数指针指向bar()

    X (*pf)();
    pf = bar;

    会被转化为:

    void (*pf)(X&);
    pf = bar;

    在使用者层面做优化

    考虑下面的代码:

    X bar(const T& y, const T& z)
    {
     X xx;
     // 使用y和z处理xx
     return xx;
    }

    对于bar(),Jonathan Shopiro提出程序员优化的概念,为X定义一个constructor,以yz作为参数:

    X bar(const T& y, const T& z)
    {
     return X(y, z);
    }

    前文已经讲过,bar()可能被转化为:

    void bar(X& _result)
    {
     _result.X::X(y, z);
     return;
    }

    由于省去了copy constructor的调用,这种方法效率更高,代价是需要额外定义一个constructor。

    在编译器层面做优化

    前面讲过了对bar()可能的转化,编译器可以使用该方法做优化,甚至可以省去对copy constructor的调用:

    X bar()
    {
     X xx;
     // ... 处理xx
     return xx;
    }

    被优化为:

    void bar(X& _result)
    {
     _result.X::X();
     // ... 处理——result
     return;
    }

    这样的优化被称为NRV(Named Return Value)优化,并在C++标准中广泛使用。下面的代码是NRV优化的一个测试用例:

    #include <iostream>
    #include <chrono>
    
    class test
    {
     friend test foo(double);
    public:
     test()
     {
         memset(array_d, 0, 100 * sizeof(double));
     }
     test(const test& t)
     {
         memcpy(this, &t, sizeof(test));
     }
    private:
     double array_d[100];
    };
    
    test foo(double val)
    {
     test local;
    
     local.array_d[0] = val;
     local.array_d[99] = val;
    
     return local;
    }
    
    int main()
    {
    
     std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
     for (int cnt = 0; cnt < 10000000; cnt++)
     {
         test t = foo(double(cnt));
     }
     std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
    
     std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "us";
    
     return 1;
    }

    只有当test定义了copy constructor时才会施行NRV优化。我的观点是,这句话需要结合bar()潜在的语义进行理解,bar()只是构造了一个X对象的实例并在处理后返回,它的实现中并没有要求调用copy constructor的语义,因此没有必要再在bar()中引入一个临时的X,通过copy constructor将它复制给真正的返回参数。所以实施了优化后,用户定义的copy constructor不会被调用。我在MSVC上进行测试,releas下copy constructor里的打印操作没有执行,而在debug下是会执行的。此外,MSVC中加入了copy constructor,并没有获得多少性能提升2333,这个现象和书中是一致的。

    是否需要copy constructor

    定义一个三维坐标点类:

    class Point3d
    {
    public:
     Point3d(float x, float y, float z);
    private:
     float m_x, m_y, m_z;
    };

    需要为其显示定义copy constructor吗?显然它的default copy constructor是trivial的,因此member-wise初始化对它来说施行的是bit-wise copy,效率高且安全,你没有理由再定义一个copy constructor。但如果情况是Point3d需要大量的member-wise初始化,比如前面的以传值被返回,就有必要实现一个copy constructor激活NRV。此时,你可能觉得直接在copy constructor中使用memcpy是个不错的主意,但请注意,这只在类中不含有任何由编译器产生的成员时才成立。否则,类似vptr之类的成员会被改写。