我想知道我的构造函数的最佳形式。下面是一些示例代码:
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 copy)
(1b) y被拷贝到X的构造函数的参数中,然后拷贝到x1.m_y中(2份)
(1c) y 移入 x1.m_y (1 move)
(2a) f() 的结果被复制到 x2.m_y (1 copy)
(2b) f() 被构造到构造函数的实参中,然后复制到x2.m_y (1 copy)
(2c) f()在栈上创建,然后移入x2.m_y(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 无法优化掉临时
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
仍在原地 build 。凉爽的。现在让我们研究新的移动语义如何影响第二个示例。
真实案例,附
-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; }
标记的位置显示了问题。对非临时对象进行了移动。那是因为右值引用的行为类似于左值引用,只是它们也可能绑定(bind)到临时对象。所以我们一定不要忘记重载
B
的构造函数带有一个接受常量左值引用的构造函数。 B (S const &s) : s_ (s)
{
std::cout << "B::<constructor2>" << std::endl;
}
然后您会注意到
g, g2
导致“constructor2”被调用,因为符号 s
在任何一种情况下,都更适合 const 引用而不是右值引用。我们可以说服编译器在
g
中做一个 Action 以两种方式之一: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")); }
那将满足strong guarantee仅当
A
的任务提供了它。复制到result
不能跳过,也不能tmp
代替 result
build , 自 result
没有被 build 。因此,它比以前慢,不需要复制。 C++0x 编译器和移动赋值运算符会减少开销,但它仍然比按值返回慢。按值(value)返回更容易提供强有力的保证。对象就地构建。如果其中一部分发生故障而其他部分已经构建,则正常展开将清理并且只要
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;
}
关于c++ - 构造函数的最佳形式?传值还是传引用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4321305/