15

以下是我从库的编码风格文档中找到的摘录:

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

DoSomething(XName("blah"));

而不是

XName n("废话"); 做某事(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,但我想知道上面提到的一般准则是否有任何道理)

4

3 回答 3

10

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

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

只专注于编写可读的代码。

您的第一个示例确实消除nDoSomething( 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, not foo,并且operator+定义为(本质上,并且自 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 ); }

不安全的风格。

接受右值引用并返回相同引用的第二个重载完全消除了中间临时(不依赖省略),允许将调用链+完美地转换为+=调用。但不幸的是,它还通过将调用链开头的剩余临时绑定到引用来取消其生命周期扩展的资格。所以返回的引用在分号之前是有效的,但是它会消失,没有什么可以阻止它。所以这主要在模板表达式库之类的东西中很有用,对哪些结果可以绑定到本地引用有记录的限制。

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

像这样评估图书馆文档需要对图书馆作者的技能进行一些评估。如果他们说要以某种古怪的方式做事,因为它总是更高效,请保持怀疑,因为 C++ 并不是故意设计为古怪的,而是设计为高效的。

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

于 2013-07-04T00:55:28.353 回答
4

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

原因是如果你有

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

// ...

T t;
foo(t);

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

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

因此,避免命名临时对象通常是更好的做法。

于 2013-07-04T20:38:12.917 回答
3

这里有几点:

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

我会以这种方式提出类似的编码指南:

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

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

于 2013-07-04T00:48:34.807 回答