【问题标题】:How to properly initialize class value member?如何正确初始化类值成员?
【发布时间】:2009-12-06 14:06:12
【问题描述】:

假设我们有这个:

class Foo {

    public:
    Foo(const Bar& b) : m_bar(b) {}

    private:
    Bar m_bar;
};

现在关于效率C++ FAQ LITE 是这样说的:

考虑以下使用初始化列表初始化成员对象“x”的构造函数:Fred::Fred() : x(whatever) { }。这样做最常见的好处是提高了性能。例如,如果表达式whatever与成员变量“x”的类型相同,则whatever表达式的结果直接在“x”内部构造——编译器不会制作对象的单独副本。

构造函数是否应该更好地将参数作为值而不是引用?

由于我使用的是构造函数初始化列表,会不会有性能差异?

最后,也是最重要的,会有语义差异吗?例如,给 new Foo(Bar()) 的调用者

谢谢

-- 编辑-- 将参数声明更正为 const 引用。

【问题讨论】:

  • 除了 Zan Lynx 之外,每个人都得出结论,通过引用传递应该更有效,因为这样可以避免额外的副本。我不是很相信,因为 C++ FAQ 另有建议,所以我做了自己的测试。看来 C++ FAQ 是对的;使用构造函数初始化列表时,引用传递和值传递没有区别。

标签: c++


【解决方案1】:

就您引用的内容而言,即使b 是 Bar 引用且 m_bar 是 Bar 对象,“如果表达式与成员变量 x_ 的类型相同”也是正确的。

按值获取参数可能会更慢,因为如果有的话,它可能会导致额外的副本。

正如 Josef 和 SoapBox 所说,引用应该是 const。否则你不能通过临时工。拥有一个修改其参数的构造函数是合法的,但这很少是一个好主意,至少在 C++0x 移动语义到来之前是这样。

【讨论】:

    【解决方案2】:

    我将按照您的要求将其分解为性能和语义差异:

    构造函数是否应该更好地将参数作为值而不是引用?

    除非是原始类型或小型结构,否则它应该具有通过 const 引用传递的参数。通过引用传递给您带来了性能差异:程序在将整个对象传递给构造函数之前不必复制整个对象。然而,对于小对象,避免复制所节省的时间可能不会被额外的间接级别所抵消。

    传递 const 确保您可以使用临时对象调用构造函数。因此,对于构造函数的调用者来说,是通过值调用还是通过 const 引用调用没有语义上的区别。

    由于我使用的是构造函数初始化列表,会不会有性能差异?

    如果你不使用初始化列表,并且你正在初始化的对象有一个默认构造函数,这就是语义上发生的

    class Foo
    {
        Bar bar;
        Foo(const &Bar bar_)
        /* bar(Bar()) is implicitly called here,
        before the start of the function body */
        {
            /* Note that we cannot do bar(bar_) now
            as bar has already been constructed.
            So we might do this instead: */
            bar = bar_; // the assignment operator function is called here
        }
    };
    

    但是,如果编译器能够看到 Bar 的默认构造函数除了初始化 bar 之外没有任何副作用,并且 bar 的值在构造函数的主体中被覆盖,它可以选择完全忽略(删除)这个调用。但是我们总是可以让编译器的工作更轻松,而不是进行额外的调用。

    请注意,如果您要初始化的对象没有默认构造函数,那么您必须在初始化列表中对其进行初始化。这是另一个语义差异。

    【讨论】:

    • bar = bar_ 中的注释应为“此处调用赋值运算符”。对象之前已经构造过,因此可以赋值(如果赋值运算符可用)但不能重构(你可以强制它,但不应该)。
    • '但是,如果编译器能够...' 在大多数情况下,编译器无法做出这样的假设。标准允许的唯一假设是复制构造函数没有副作用。删除副本需要完全验证什么 Bar 构造函数和所有 Bar 子对象的构造函数没有副作用,这本身会非常昂贵,如果这些构造函数中的任何一个在不同的编译单元中实现也是不可能的......即使它理论上是可能的,编译器实现者几乎不可能做到。
    • +1 无论如何,它涵盖了答案,并且确实带来了对可用 Bar 默认构造函数的依赖。
    • 糟糕,感谢您指出关于赋值运算符的错误。关于省略:我实际上尝试了一些简单的测试用例。如果默认 ctor 和赋值运算符都纯粹使用编译时常量修改对象,那么 gcc 似乎会省略不必要的。但是,似乎一旦在赋值运算符中完成了实际复制,就不再发生省略。
    • (续)我认为编译器试图解释自赋值的可能性。因此,编译器的问题似乎在于复制过程,而不是弄清楚构造函数的作用。
    【解决方案3】:

    如果按值传递,则在将参数传递给构造函数时会进行复制,并在初始化成员时进行第二次复制。使用引用会删除副本。但是,您应该使用 const 引用。引用会阻止您调用Foo(Bar()),因为临时对象不能绑定到非常量引用。

    【讨论】:

      【解决方案4】:

      您应该通过 const 引用传递它,因为这样可以避免不必要的复制。但是,引用是 const 很重要,否则编译器必须期望对象可以被构造函数修改。

      如果您通过非常量引用传递参数,如在您的示例中,根据 C++ 标准,语句 new Foo(Bar()) 是不允许的(您不能采用非对 r 值的 const 引用——也就是说,位于赋值右侧的东西)。无论如何,一些编译器会允许它,但这是非标准的。

      【讨论】:

        【解决方案5】:

        允许 C++ 删除副本。这是在返回值优化中完成的。

        更新!

        原来我错了!

        在某一点上我是对的,但 C++ 委员会在 1997 年否认了编译器的优化。无论我记得哪个编译器这样做都是过时的,或者做错了,或者可能决定无视委员会。 (当然看不出有任何理由需要额外的副本!)

        在更新的总结中,C++ 只允许省略(删除)复制构造函数操作,用于返回值优化以及抛出和接收异常对象。

        更新 2

        我又错了?显然我无法正确阅读标准文档?

        GCC 和 Microsoft cl.exe 都生成结果,无需额外的临时副本。

        这是我的测试代码,全部微软化:

        #include <stdio.h>
        #include <tchar.h>
        
        class A {
        int m;
        public:
        A(int x=0) : m(x)
        {
            _putts(_T("Constructor A"));
        }
        A(const A &x) : m(x.m)
        {
            _putts(_T("Copy A"));
        }
        int get() const { return m; }
        };
        
        class B {
        A m;
        public:
        B(int y=5) : m(y)
        {
            _putts(_T("Constructor B"));
        }
        B(const B &x) : m(x.m)
        {
            _putts(_T("Copy B"));
        }
        B(A x) : m(x)
        {
            _putts(_T("Construct B from A value"));
        }
        int get() const { return m.get(); }
        };
        
        class C {
        A m;
        public:
        C(int y=5) : m(y)
        {
            _putts(_T("Constructor C"));
        }
        C(const C &x) : m(x.m)
        {
            _putts(_T("Copy C"));
        }
        C(const A &x) : m(x)
        {
            _putts(_T("Construct C from A reference"));
        }
        int get() const { return m.get(); }
        };
        
        int _tmain(int argc, _TCHAR* argv[])
        {
            _putts(_T("Hello World"));
        
        int i = 27;
        if( argc > 1 )
            i = _tstoi(argv[0]);
        
        A aval(i);
        _tprintf(_T("A value is: %d\n"), aval.get());
        
        B bval( aval );
        _tprintf(_T("Value is: %d\n"), bval.get());
        
        B bval2( (A(i)) );
        _tprintf(_T("Value is: %d\n"), bval2.get());
        
        C cval( aval );
        _tprintf(_T("Value is: %d\n"), cval.get());
        
        C cval2( (A(i)) );
        _tprintf(_T("Value is: %d\n"), cval2.get());
        
        _putts(_T("Goodbye World"));
        return 0;
        }
        

        使用它来替换 GCC 的 de-Microsofting 的包含:

        #include <cstdio>
        #include <cstdlib>
        using namespace std;
        
        #define _TCHAR char
        #define _T(x) x
        #define _tmain main
        #define _putts puts
        #define _tprintf printf
        #define _tstoi atoi
        

        这是两个编译器的输出:

        Hello World
        Constructor A
        A value is: 27
        Copy A
        Copy A
        Construct B from A value
        Value is: 27
        Constructor A
        Copy A
        Construct B from A value
        Value is: 27
        Copy A
        Construct C from A reference
        Value is: 27
        Constructor A
        Copy A
        Construct C from A reference
        Value is: 27
        Goodbye World
        

        【讨论】:

        • 好的,未选中答案;但是,您愿意尝试以下测试用例吗? zoomblab.blogspot.com/2009/12/…
        • 在我的机器上,这两种情况的行为似乎完全相同,只创建了一个对象——除了类成员本身。
        【解决方案6】:
        1. 不,你想要它的方式*。
        2. 如果您将 this 作为值而不是引用传递,则会调用 Bar() 上的复制构造函数,这会影响性能(取决于复制构造函数的复杂程度)。

        *注意:你拥有它的方式,你不能这样做new Foo(Bar()),因为你不能传递对 Bar() 的引用,你需要在一个变量中有一个对象实例。

        【讨论】:

          猜你喜欢
          • 2019-09-10
          • 2019-10-14
          • 1970-01-01
          • 2011-03-08
          • 1970-01-01
          • 2018-11-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多