我整理了一些例子。我在所有这些中都使用了 GCC 4.4.4。
简单的案例,没有-std=c++0x
首先,我将一个非常简单的示例放在一起,其中包含两个接受std::string
每个类的类。
#include <string>
#include <iostream>
struct A /* construct by reference */
{
std::string s_;
A (std::string const &s) : s_ (s)
{
std::cout << "A::<constructor>" << std::endl;
}
A (A const &a) : s_ (a.s_)
{
std::cout << "A::<copy constructor>" << std::endl;
}
~A ()
{
std::cout << "A::<destructor>" << std::endl;
}
};
struct B /* construct by value */
{
std::string s_;
B (std::string s) : s_ (s)
{
std::cout << "B::<constructor>" << std::endl;
}
B (B const &b) : s_ (b.s_)
{
std::cout << "B::<copy constructor>" << std::endl;
}
~B ()
{
std::cout << "B::<destructor>" << std::endl;
}
};
static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }
int main ()
{
A a (f ());
A a2 (f2 ());
B b (g ());
B b2 (g2 ());
return 0;
}
该程序的输出stdout
如下:
A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>
结论
GCC 能够优化每一个临时A
或B
离开。
这与C++ FAQ一致。基本上,GCC 可能(并且愿意)生成a, a2, b, b2
就地构造的代码,即使调用了一个看起来按值返回的函数。因此,GCC 可以避免通过查看代码可能已经“推断”出的许多临时变量。
接下来我们要查看的是std::string
在上面的示例中实际复制的频率。让我们std::string
用我们可以更好地观察和看到的东西来代替。
真实案例,无-std=c++0x
#include <string>
#include <iostream>
struct S
{
std::string s_;
S (std::string const &s) : s_ (s)
{
std::cout << " S::<constructor>" << std::endl;
}
S (S const &s) : s_ (s.s_)
{
std::cout << " S::<copy constructor>" << std::endl;
}
~S ()
{
std::cout << " S::<destructor>" << std::endl;
}
};
struct A /* construct by reference */
{
S s_;
A (S const &s) : s_ (s) /* expecting one copy here */
{
std::cout << "A::<constructor>" << std::endl;
}
A (A const &a) : s_ (a.s_)
{
std::cout << "A::<copy constructor>" << std::endl;
}
~A ()
{
std::cout << "A::<destructor>" << std::endl;
}
};
struct B /* construct by value */
{
S s_;
B (S s) : s_ (s) /* expecting two copies here */
{
std::cout << "B::<constructor>" << std::endl;
}
B (B const &b) : s_ (b.s_)
{
std::cout << "B::<copy constructor>" << std::endl;
}
~B ()
{
std::cout << "B::<destructor>" << std::endl;
}
};
/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }
/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }
/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }
/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }
int main ()
{
A a (f ());
std::cout << "" << std::endl;
A a2 (f2 ());
std::cout << "" << std::endl;
B b (g ());
std::cout << "" << std::endl;
B b2 (g2 ());
std::cout << "" << std::endl;
return 0;
}
不幸的是,输出符合预期:
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
S::<copy constructor>
B::<constructor>
S::<destructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
S::<copy constructor>
B::<constructor>
S::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
结论
GCC无法优化掉由的构造函数S
创建的临时变量。B
使用默认的复制构造函数S
并没有改变这一点。改变f, g
成为
static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!
确实有指定的效果。似乎 GCC 愿意在适当的位置构造 'B
构造函数的参数,但不愿在适当的位置构造B
' 的成员。请注意,仍然没有临时A
或被B
创建。这意味着a, a2, b, b2
仍在原地建造。凉爽的。
现在让我们研究一下新的移动语义如何影响第二个例子。
现实案例,与-std=c++0x
考虑将以下构造函数添加到S
S (S &&s) : s_ ()
{
std::swap (s_, s.s_);
std::cout << " S::<move constructor>" << std::endl;
}
B
并将的构造函数更改为
B (S &&s) : s_ (std::move (s)) /* how many copies?? */
{
std::cout << "B::<constructor>" << std::endl;
}
我们得到这个输出
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<move constructor>
B::<constructor>
S::<destructor>
S::<constructor>
S::<move constructor>
B::<constructor>
S::<destructor>
B::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
因此,我们可以通过使用右值传递来用两个移动替换四个副本。
但我们实际上构建了一个损坏的程序。
记起g, g2
static B g () { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }
标记的位置显示了问题。对非临时对象进行了移动。这是因为右值引用的行为类似于左值引用,只是它们也可能绑定到临时对象。所以我们不能忘记B
用一个接受常量左值引用的构造函数重载。
B (S const &s) : s_ (s)
{
std::cout << "B::<constructor2>" << std::endl;
}
然后您会注意到两者都会g, g2
导致调用“constructor2”,因为s
在任何一种情况下,符号都更适合 const 引用而不是右值引用。g
我们可以通过以下两种方式之一说服编译器进行移动:
static B g () { return B (S ("string")); }
static B g () { S s ("string"); return B (std::move (s)); }
结论
按值返回。该代码将比“填写我给你的参考”代码更具可读性,并且速度更快,甚至可能更安全。
考虑f
改为
static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }
只有在's assignment 提供的情况下,这才会满足强有力的保证。A
复制 intoresult
不能被跳过,也不tmp
能被构造来代替result
,因为result
没有被构造。因此,它比以前慢,以前不需要复制。C++0x 编译器和移动赋值运算符会减少开销,但它仍然比按值返回要慢。
价值回报更容易提供强有力的保证。对象是就地构造的。如果其中一部分失败而其他部分已经构建,则正常展开将清理,并且只要S
构造函数对其自身成员履行基本保证和对全局项的强保证,则全部返回价值处理实际上提供了强有力的保证。
如果要复制(到堆栈上),请始终按值传递
如想要速度?按值传递。. 编译器可能会生成代码,如果可能的话,在适当的位置构造调用者的参数,从而消除复制,当您通过引用获取然后手动复制时它无法做到这一点。主要示例:不要写这个(取自引用的文章)
T& T::operator=(T const& x) // x is a reference to the source
{
T tmp(x); // copy construction of tmp does the hard work
swap(*this, tmp); // trade our resources for tmp's
return *this; // our (old) resources get destroyed with tmp
}
但总是喜欢这个
T& T::operator=(T x) // x is a copy of the source; hard work already done
{
swap(*this, x); // trade our resources for x's
return *this; // our (old) resources get destroyed with x
}
如果要复制到非堆栈帧位置,请通过 C++0x 之前的 const 引用传递,另外通过 C++0x 之后的右值引用传递
我们已经看到了这一点。与按值传递相比,当无法进行就地构造时,按引用传递会导致发生的副本更少。并且 C++0x 的移动语义可以用更少和更便宜的移动来代替许多副本。但请记住,移动会使已移动的对象变成僵尸。移动不是复制。只提供一个接受右值引用的构造函数可能会破坏事情,如上所示。
如果您想复制到非堆栈帧位置并拥有swap
,请考虑按值传递(C++0x 之前)
如果你有便宜的默认构造,那么结合 aswap
可能比复制东西更有效。考虑S
的构造函数是
S (std::string s) : s_ (/* is this cheap for your std::string? */)
{
s_.swap (s); /* then this may be faster than copying */
std::cout << " S::<constructor>" << std::endl;
}