构造函数的最佳形式?通过值或引用传递?
Posted
技术标签:
【中文标题】构造函数的最佳形式?通过值或引用传递?【英文标题】:Best form for constructors? Pass by value or reference? 【发布时间】:2011-05-18 07:17:43 【问题描述】:我想知道构造函数的最佳形式。下面是一些示例代码:
class Y ...
class X
public:
X(const Y& y) : m_y(y) // (a)
X(Y y) : m_y(y) // (b)
X(Y&& y) : m_y(std::forward<Y>(y)) // (c)
Y m_y;
Y f() return ...
int main()
Y y = f();
X x1(y); // (1)
X x2(f()); // (2)
据我了解,这是编译器在每种情况下所能做的最好的事情。
(1a) y 被复制到 x1.m_y(1 份)
(1b) y复制到X的构造函数的参数中,然后复制到x1.m_y(2份)
(1c) y 移动到 x1.m_y (1 move)
(2a) f() 的结果被复制到 x2.m_y(1 份)
(2b) f()被构造成构造函数的参数,然后复制到x2.m_y(1份)
(2c) f() 在栈上创建,然后移入 x2.m_y (1 move)
现在有几个问题:
在这两个方面,通过 const 引用传递并不差,有时甚至比通过值传递更好。这似乎与"Want Speed? Pass by Value." 上的讨论背道而驰。对于 C++(不是 C++0x),我应该坚持这些构造函数的 const 引用传递,还是应该按值传递?对于 C++0x,我应该通过右值引用而不是按值传递吗?
对于 (2),我更喜欢将临时对象直接构建到 x.m_y 中。即使是我认为的右值版本也需要移动,除非对象分配动态内存,否则它的工作量与副本一样多。有没有办法对此进行编码,以便允许编译器避免这些复制和移动?
在我认为编译器可以做得最好的方面以及我的问题本身方面,我都做了很多假设。如有错误,请更正。
【问题讨论】:
M m_y;
应该表示Y m_y;
还是M
可以从Y
构造?在(2a)x2
的情况下,根据C++ FAQ,通常是直接构造的,没有任何副本或临时性。
这一行是“Y y = f();”吗不是错误?由于 f() 的返回类型是 X。Y 的任何构造函数都将 X 作为参数吗?好像!
这一切都取决于。没有一个正确答案,视情况而定。
这里需要问的问题是,“最好”对你来说意味着什么?
dennycrane, Nawaz:抱歉,我已经修复了你提到的错误。谢谢。
【参考方案1】:
我整理了一些例子。我在所有这些中都使用了 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 无法优化掉由B
的构造函数创建的临时S
。使用 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"));
只有当A
的任务提供它时,它才会满足strong guarantee。不能跳过复制到result
,也不能构造tmp
来代替result
,因为没有构造result
。因此,它比以前慢,以前不需要复制。 C++0x 编译器和移动赋值运算符会减少开销,但仍然比按值返回要慢。
按值返回更容易提供强有力的保证。对象是就地构造的。如果其中一部分失败而其他部分已经构建,则正常展开将清理,并且只要S
的构造函数对其自身成员履行基本保证和对全局项的强保证,整个价值回报流程实际上提供了强有力的保障。
如果要复制(到堆栈上),请始终按值传递
如Want speed? Pass by value. 中所述。编译器可能会生成代码,如果可能的话,在适当的位置构造调用者的参数,从而消除复制,当您通过引用获取然后手动复制时它无法做到这一点。主要示例: 不要不写这个(取自引用的文章)
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 之前)
如果你有廉价的默认构造,结合swap
可能比复制东西更有效。考虑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;
【讨论】:
-g -O2
在所有这些中都启用了。
嗨,dennycrane,感谢您的精彩回复!只有一个问题,关于“如果你要复制,总是按值传递”,假设你有以下内容:“T1 x; T2 y(x);”其中 T2 有一个类型为 T1 的成员,如果 T2 的构造函数是 const 引用,则有一份副本(进入成员),但如果 T2 的构造函数是按值,则有一份副本(进入函数争论)和一个移动(进入会员)。那么如果复制到成员变量中,“如果要复制,则始终按值传递”是否适用?
另外请注意,我对 GCC 做了一些进一步的测试(我将很快发布一些源代码)。基本上,我做了 X x = f(f(f(f(20)))) 然后 X* x_p = &x,然后查看了 x_p 周围的内存(通过说 *(x_p + 2))来查看哪些临时被创建(即查看现在无效的堆栈空间)。我用-O0 和-O3 做到了这一点。在 -O3 处,gcc 似乎在消除所有临时变量方面做得很好,无论您是按引用传递还是按值传递。以上是关于构造函数的最佳形式?通过值或引用传递?的主要内容,如果未能解决你的问题,请参考以下文章