要了解为什么这是一个好的模式,我们应该检查 C++03 和 C++11 中的替代方案。
我们有 C++03 获取std::string const&的方法:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
在这种情况下,将始终执行单个副本。如果从原始 C 字符串构造,将构造 std::string,然后再次复制:两次分配。
有一种 C++03 方法可以引用 std::string,然后将其交换为本地 std::string:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
这是“移动语义”的 C++03 版本,swap 通常可以优化为非常便宜(很像move)。它也应该在上下文中分析:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
并强制你形成一个非临时的std::string,然后丢弃它。 (临时的std::string 不能绑定到非常量引用)。但是,只完成了一次分配。 C++11 版本将采用 && 并要求您使用 std::move 或临时调用它:这要求调用者显式在调用之外创建一个副本,并且将该副本移动到函数或构造函数中。
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
用途:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
接下来,我们可以做完整的 C++11 版本,支持复制和move:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
然后我们可以检查它是如何使用的:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
很明显,这种 2 重载技术至少与上述两种 C++03 样式一样有效,甚至更高。我将这个 2-overload 版本称为“最佳”版本。
现在,我们将检查按副本获取的版本:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
在每种情况下:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
如果您将此与“最佳”版本并排比较,我们会多做一个move!我们没有一次额外的copy。
因此,如果我们假设 move 很便宜,那么这个版本的性能几乎与最优化版本相同,但代码量减少了 2 倍。
如果您使用 2 到 10 个参数,代码的减少是指数级的 - 1 个参数减少 2 倍,2 减少 4 倍,3 减少 8 倍,4 减少 16 倍,10 参数减少 1024 倍。
现在,我们可以通过完美转发和 SFINAE 解决这个问题,允许您编写一个带有 10 个参数的构造函数或函数模板,执行 SFINAE 以确保参数是适当的类型,然后移动或复制他们根据需要进入当地状态。虽然这可以防止程序大小增加千倍的问题,但仍然可以从这个模板生成一大堆函数。 (模板函数实例化生成函数)
大量生成的函数意味着更大的可执行代码大小,这本身会降低性能。
以几个moves 为代价,我们获得了更短的代码和几乎相同的性能,而且通常更容易理解代码。
现在,这只是因为我们知道,当调用函数(在本例中为构造函数)时,我们将需要该参数的本地副本。这个想法是,如果我们知道我们将要制作一个副本,我们应该通过将它放在我们的参数列表中来让调用者知道我们正在制作一个副本。然后他们可以围绕他们将给我们一份副本这一事实进行优化(例如,通过进入我们的论点)。
“按值取值”技术的另一个优点是移动构造函数通常是 noexcept。这意味着按值取值并移出其参数的函数通常可以是 noexcept,将任何 throws 移出其参数body 并进入调用范围(有时可以通过直接构造来避免它,或者将项目和move 构造到参数中,以控制发生抛出的位置)。使方法 nothrow 通常是值得的。