命名对象与临时对象:在可能的情况下避免命名对象是否更好?

时间:2013-07-04 00:38:53

标签: c++

以下是我从图书馆的编码样式文档中找到的摘录:

  

在可能的情况下,最好使用临时而不是   存储命名对象,例如:

     

DoSomething(XName(“blah”));

     

而不是

     

XName n(“blah”); DoSomething(n);

     

因为这使得编译器更容易优化调用   减少功能的堆栈大小等。不要忘记考虑   然而,临时的生命周期。

假设不需要修改对象且生命周期问题不成问题,本指南是否正确?我当时认为在这个时代它不会有所作为。但是,在某些情况下,您无法避免命名对象:

XName n("blah");
// Do other stuff to mutate n
DoSomething( n );

另外,使用移动语义,我们可以编写这样的代码,因为临时代码被删除了:

std::string s1 = ...;
std::string s2 = ...;
std::string s3 = ...;
DoSomething( s1 + s2 + s3 );

而不是(我听说编译器可以使用C ++ 03中的以下内容更好地进行优化):

std::string s1 = ...;
std::string s2 = ...;
std::string s3 = ...;
s1 += s2; 
s1 += s3;  // Instead of s1 + s2 + s3
DoSomething(s1);

(当然,上面的内容可以归结为measure and see for yourself,但我想知道上面提到的一般准则是否有任何道理呢?

3 个答案:

答案 0 :(得分:10)

编译器前端的主要工作是从所有内容中删除名称以解析底层语义结构。

倾向于避免使用名称确实有助于避免不必要地获取对象的地址,这可能会无意中阻止编译器操纵数据。但是有足够的方法来获得一个临时的地址,这几乎没有实际意义。命名对象的特殊之处在于它们不适合C ++中的构造函数省略,但正如您所提到的,移动语义消除了最昂贵的不必要的复制构造。

专注于编写可读代码。

您的第一个示例确实删除了n的副本,但在C ++ 11中,您可以使用移动语义:DoSomething( std::move( n ) )

在示例s1 + s2 + s3中,C ++ 11使事情更有效率也是如此,但移动语义和消除临时性是不同的事情。移动构造函数只是构建一个更便宜的临时构造。

我也受到了误解,即C ++ 11会消除临时性,只要你使用成语

// What you should use in C++03
foo operator + ( foo lhs, foo const & rhs ) { return lhs += rhs; }

这实际上是不真实的; lhs是一个命名对象,而不是临时对象,它不符合复制省略的返回值优化形式。事实上,在C ++ 11中,这将产生一个副本,而不是一个动作!您需要使用std::move( lhs += rhs );修复此问题。

// What you should use in C++11
foo operator + ( foo lhs, foo const & rhs ) { return std::move( lhs += rhs ); }

您的示例使用std::string,而不是foooperator+已定义(基本上,自C ++ 03以来)

// What the C++03 Standard Library uses
string operator + ( string const & lhs, string const & rhs )
    { return string( lhs ) += rhs; } // Returns rvalue expression, as if moved.

此策略具有与上述类似的属性,因为一旦绑定到引用,临时将被取消资格省略。有两种可能的修复方法,可以在速度和安全性之间进行选择。这两个修补程序都不兼容第一个成语,move已经实现了安全风格(因此你应该使用它!)。

安全风格。

这里没有命名对象,但是lhs参数的临时绑定不能直接构造到结果绑定到引用停止副本省略。

// What the C++11 Standard Library uses (in addition to the C++03 library style)
foo operator + ( foo && lhs, foo const & rhs )
    { return std::move( lhs += rhs ); }

不安全的风格。

接受rvalue引用并返回相同引用的第二个重载完全消除了中间临时(不依赖于elision),允许+个调用链完全转换为+=个调用。但不幸的是,它还通过将其绑定到引用来取消在生命周期扩展的调用链开始时剩余的临时值。因此返回的引用在分号之前有效,但随后它就会消失,没有什么可以阻止它。因此,这在模板表达式库之类的内容中非常有用,并且对可以绑定到本地引用的结果有文档限制。

// No temporary, but don't bind this result to a local!
foo && operator + ( foo && lhs, foo const & rhs )
    { return std::move( lhs += rhs ); }

评估图书馆文档需要对图书馆作者的技能进行一些评估。如果他们说以某种古怪的方式做事情因为它总是更有效率,那就要持怀疑态度,因为C ++并不是故意设计成古怪的,但它的设计是有效的。

然而,在表达式模板的情况下,临时包含复杂类型的计算,这些计算会被赋值给具体类型的命名变量而中断,你应该绝对听取作者所说的内容。在这种情况下,他们可能会更有知识。

答案 1 :(得分:4)

我认为接受的答案是错误的。避免命名临时对象 更好。

原因是如果你有

struct T { ... };
T foo(T obj) { return obj; }

// ...

T t;
foo(t);

然后t将被复制构造,如果复制构造函数具有可观察的副作用,则无法优化

相比之下,如果您说过foo(T()),则可以完全避免调用复制构造函数,无论潜在的副作用。

因此,一般来说,避免命名临时对象是更好的做法。

答案 2 :(得分:3)

以下是几点:

  • 您永远无法确切知道哪些编译器会优化,哪些不会。优化是一件复杂的事情。优化器编写者往往非常小心,不要破坏某些东西。有可能遇到优化器错误地决定不应该优化某些内容的错误。编译器中的编码标准非常高。然而,它们是由人类写的。
  • 这种特殊的编码风格摘录似乎并不合理。我们的日子编译器几乎总是很好。很难想象优化器会混淆XName n("blah"); DoSomething(n);中的某些内容 - 这段代码太简单了。

我会用这样的方式编写类似的编码指南:

  • 以易于理解和修改的方式编写代码;
  • 一旦观察到性能问题,请查看生成的代码并进行思考 如何取悦编译器。

最好按此顺序解决问题,而不是相反的方式。