C++ 什么时候重载按引用传递(l值和r值)比按值传递更可取?
我看到有人说过,在C++11中,编写一个C++ 什么时候重载按引用传递(l值和r值)比按值传递更可取?,c++,c++11,overloading,assignment-operator,copy-and-swap,C++,C++11,Overloading,Assignment Operator,Copy And Swap,我看到有人说过,在C++11中,编写一个操作符=以按值获取相同类型的参数,既可以作为复制赋值操作符,也可以作为移动赋值操作符: Foo& operator=(Foo f) { swap(f); return *this; } 如果备选方案的行数是原来的两倍多,代码重复次数多,并且可能出现错误: Foo& operator=(const Foo& f) { Foo f2(f); swap(f2); return *this; }
操作符=
以按值获取相同类型的参数,既可以作为复制赋值操作符,也可以作为移动赋值操作符:
Foo& operator=(Foo f)
{
swap(f);
return *this;
}
如果备选方案的行数是原来的两倍多,代码重复次数多,并且可能出现错误:
Foo& operator=(const Foo& f)
{
Foo f2(f);
swap(f2);
return *this;
}
Foo& operator=(Foo&& f)
{
Foo f2(std::move(f));
swap(f2);
return *this;
}
在什么情况下,引用常量和r值重载比
按值传递,或何时需要?我在考虑std::vector::push_back,
例如,定义为两个重载:
void push_back (const value_type& val);
void push_back (value_type&& val);
下面是第一个示例,其中传递值用作副本赋值
运算符和移动分配运算符,无法在中定义
标准是一个单一的功能吗
void push_back (value_type val);
对于副本分配运算符可以回收资源的类型,使用副本交换几乎从来都不是实现副本分配运算符的最佳方式。例如,查看
std::vector
:
此类管理动态大小的缓冲区,并维护容量(缓冲区可容纳的最大长度)和大小(当前长度)。如果实现了向量
复制分配运算符交换
,则无论发生什么情况,如果rhs.size()!=0
但是,如果lhs.capacity()>=rhs.size()
,则根本不需要分配新的缓冲区。可以简单地将元素从rhs
分配/构造到lhs
。当元素类型是可复制的时,这可能归结为除了memcpy
之外什么都没有。这可能比分配和释放缓冲区快得多
std::string
也有同样的问题
当MyType
的数据成员为std::vector
和/或std::string
时,MyType
也存在同样的问题
只有2次你想考虑用SWAP实现拷贝分配:
您知道swap
方法(包括当rhs是左值时的强制复制构造)不会非常低效
您知道,您将始终需要复制分配操作员具有强大的异常安全保证
如果您对2不确定,换句话说,您认为复制分配操作符有时可能需要强大的异常安全保证,请不要在交换方面实现分配。如果您提供以下其中一项,您的客户很容易获得相同的担保:
无例外的交换
noexcept移动赋值运算符
例如:
template <class T>
T&
strong_assign(T& x, T y)
{
using std::swap;
swap(x, y);
return x;
}
想象一下向量
,其中:
class big_legacy_type
{
public:
big_legacy_type(const big_legacy_type&); // expensive
// no move members ...
};
如果我们有:
void push_back(value_type val);
然后,push_back
将左值big_legacy_类型
放入向量
需要2份而不是1份,即使容量足够。就性能而言,这将是一场灾难
更新
这是一个HelloWorld,您应该能够在任何符合C++11标准的平台上运行:
#include <vector>
#include <random>
#include <chrono>
#include <iostream>
class X
{
std::vector<int> v_;
public:
explicit X(unsigned s) : v_(s) {}
#if SLOW_DOWN
X(const X&) = default;
X(X&&) = default;
X& operator=(X x)
{
v_.swap(x.v_);
return *this;
}
#endif
};
std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);
std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
auto t0 = std::chrono::high_resolution_clock::now();
x = y;
auto t1 = std::chrono::high_resolution_clock::now();
return t1-t0;
}
int
main()
{
const int N = 1000000;
typedef std::chrono::duration<double, std::nano> nano;
nano ns(0);
for (int i = 0; i < N; ++i)
{
X x1(size(eng));
X x2(size(eng));
ns += test(x1, x2);
}
ns /= N;
std::cout << ns.count() << "ns\n";
}
通过这个简单的测试,我看到复制/交换习惯用法的性能提高了43%。YMMV
平均而言,上述测试有一半时间在lhs上具有足够的容量。如果我们把这一点推向极端:
lhs始终具有足够的容量
lhs在任何时候都没有足够的容量
那么默认拷贝分配相对于拷贝/交换习惯用法的性能优势从560%到0%不等。复制/交换习惯用法的速度永远不会更快,而且可能会显著减慢(对于本测试)
想要速度吗?测量。当对象很难移动(例如,包含大量数据成员)时,按值形式可能更昂贵(两次移动而不是一次)。这可能部分是历史原因,因为std::vector
在C++03中已经有T const&
重载。可能存在依赖于现有重载的代码(比如有人获取了成员函数的地址)。还请注意,在该标准中,不需要优化必须实现的代码行,因为它由编译器实现者编写一次,但在其他地方几乎都使用。开发的额外成本可以忽略不计。出于明显的原因,复制/移动构造函数是绝对必要的一种情况。复制省略会有帮助吗?在第二种情况下,这会删除另一个副本吗?我想说的是,当rhs是左值时,ThanksIt没有帮助。从技术上讲,那篇文章没有错。然而,这给人留下了错误的印象:总是按价值复制是可怕的建议。就这一点而言,“任何事”都是可怕的建议。这篇文章应该给您留下这样的印象:有时候,一个by-value参数是可以的,甚至是最好的。但是太多的人忽视了“有时”,因为文章没有阐明这一点。在工具箱中保留传递值。但是在默认情况下,如果不进行设计,使用它会带来性能问题。请注意,@HowardHinnant的push\u back(const-value\u-type&)
示例很难正确编写,因为参数可能是一个元素,或者更糟的是,它属于向量的一个元素。
void push_back(value_type val);
#include <vector>
#include <random>
#include <chrono>
#include <iostream>
class X
{
std::vector<int> v_;
public:
explicit X(unsigned s) : v_(s) {}
#if SLOW_DOWN
X(const X&) = default;
X(X&&) = default;
X& operator=(X x)
{
v_.swap(x.v_);
return *this;
}
#endif
};
std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);
std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
auto t0 = std::chrono::high_resolution_clock::now();
x = y;
auto t1 = std::chrono::high_resolution_clock::now();
return t1-t0;
}
int
main()
{
const int N = 1000000;
typedef std::chrono::duration<double, std::nano> nano;
nano ns(0);
for (int i = 0; i < N; ++i)
{
X x1(size(eng));
X x2(size(eng));
ns += test(x1, x2);
}
ns /= N;
std::cout << ns.count() << "ns\n";
}
$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp
$ a.out
428.348ns
$ a.out
438.5ns
$ a.out
431.465ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp
$ a.out
617.045ns
$ a.out
616.964ns
$ a.out
618.808ns