何时重载按引用传递(左值和右值)优于按值传递?

Posted

技术标签:

【中文标题】何时重载按引用传递(左值和右值)优于按值传递?【英文标题】:When is overloading pass by reference (l-value and r-value) preferred to pass-by-value? 【发布时间】:2013-08-20 15:06:12 【问题描述】:

我看到它说 operator= 编写为按值获取相同类型的参数,在 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)

    Foo f2(std::move(f));
    swap(f2);
    return *this;

在什么情况下 ref-to-const 和 r-value 重载优于 按值传递,或者什么时候需要?我在考虑std::vector::push_back, 例如定义为两个重载:

void push_back (const value_type& val);
void push_back (value_type&& val);

在第一个示例之后,按值传递用作复制分配 运算符和移动赋值运算符,不能在中定义push_back 标准是单一功能吗?

void push_back (value_type val);

【问题讨论】:

当对象移动很重(例如包含大量数据成员)时,按值形式可能会更昂贵(两次移动而不是一次)。 这可能部分是历史原因,因为 std::vector 在 C++03 中已经有 T const& 重载。那里可能存在依赖于现有重载的代码(比如有人获取了成员函数的地址)。另请注意,在标准中,无需优化必须实现的代码行,因为它由编译器实现者编写一次,但几乎在其他任何地方都使用。额外的开发成本可以忽略不计。 绝对必要的一种情况是复制/移动构造函数,原因很明显。 【参考方案1】:

对于复制赋值运算符可以回收资源的类型,使用副本交换几乎从来都不是实现复制赋值运算符的最佳方式。例如看std::vector

这个类管理一个动态大小的缓冲区并同时维护capacity(缓冲区可以容纳的最大长度)和size(当前长度)。如果vector复制赋值运算符实现swap,那么无论如何,如果rhs.size() != 0总是分配一个新的缓冲区。

但是,如果是lhs.capacity() >= rhs.size(),则根本不需要分配新的缓冲区。可以简单地分配/构造从rhslhs 的元素。当元素类型可以简单地复制时,这可能归结为memcpy。这比分配和释放缓冲区要快得多,很多

std::string 的同样问题。

MyType 的数据成员为std::vector 和/或std::string 时,MyType 也会出现同样的问题。

只有 2 次你想考虑使用交换实现复制分配:

    您知道swap 方法(包括当rhs 为左值时的强制复制构造)不会非常低效。

    你知道你将总是需要复制赋值操作符有强大的异常安全保证。

如果您不确定 2,换句话说,您认为复制赋值运算符可能有时需要强大的异常安全保证,请不要在交换方面实现赋值。如果您提供以下之一,您的客户很容易获得相同的保证:

    无例外交换。 一个 noexcept 移动赋值运算符。

例如:

template <class T>
T&
strong_assign(T& x, T y)

    using std::swap;
    swap(x, y);
    return x;

或:

template <class T>
T&
strong_assign(T& x, T y)

    x = std::move(y);
    return x;

现在将有一些类型使用交换实现复制分配是有意义的。然而,这些类型将是例外,而不是规则。

开启:

void push_back(const value_type& val);
void push_back(value_type&& val);

想象vector&lt;big_legacy_type&gt; 在哪里:

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_type 转换为 vector 将需要 2 个副本而不是 1 个,即使 capacity 已足够。就性能而言,那将是一场灾难。

更新

这是一个你应该能够在任何符合 C++11 的平台上运行的 HelloWorld:

#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";

我用两种方式编写了X 的复制赋值运算符:

    隐式,相当于调用vector的复制赋值运算符。 使用复制/交换习语,暗示在宏SLOW_DOWN 下。我曾想过将其命名为 SLEEP_FOR_AWHILE,但如果您使用的是电池供电的设备,这种方式实际上比睡眠语句要糟糕得多。

测试在 0 到 1000 之间构建一些随机大小的 vector&lt;int&gt;s,并将它们分配一百万次。它对每个时间进行计时,对时间求和,然后以浮点纳秒为单位找到平均时间并将其打印出来。如果对高分辨率时钟的两次连续调用未返回小于 100 纳秒的时间,您可能需要增加向量的长度。

这是我的结果:

$ 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

通过这个简单的测试,我发现复制/交换习语的性能下降了 43%。 YMMV。

平均而言,上述测试在 lhs 一半的时间内有足够的容量。如果我们把它带到任何一个极端:

    lhs 始终有足够的容量。 lhs 在任何时候都没有足够的容量。

那么默认复制分配相对于复制/交换习惯用法的性能优势从大约 560% 到 0% 不等。复制/交换习语永远不会更快,并且可能会非常慢(对于这个测试)。

想要速度?测量。

【讨论】:

复制省略会有帮助吗?在第二种情况下会删除 other 副本吗?我正在尝试与cpp-next.com/archive/2009/08/want-speed-pass-by-value 达成一致,谢谢 当 rhs 是左值时它没有帮助。从技术上讲,那篇文章没有任何问题。然而,它给人留下了错误的印象:总是按价值复制是糟糕的建议。就此而言,“任何事情”总是很糟糕的建议。这篇文章应该给您留下这样的印象:有时按值参数可以,甚至是最好的。但是太多的人忽略了“有时”,因为文章没有澄清这一点。在您的工具箱中保留传递值。但是默认使用它,没有设计,是要求性能问题。 请注意,@HowardHinnant 的push_back(const value_type&amp;) 示例非常当参数可以是一个元素,或者更糟的是,由向量的一个元素拥有时,很难正确编写。跨度> 这是否意味着将移动和复制赋值定义为单个函数的复制和交换习语对于任何包含字符串的类(例如)效率低? 一般来说,是的。例外情况包括具有字符串的类其他成员,其中其他成员从复制/交换中受益匪浅。这个答案旨在推翻复制/交换始终是实现分配的最佳方式的观念。但我也想小心不要说复制/交换从不是一个好工具。

以上是关于何时重载按引用传递(左值和右值)优于按值传递?的主要内容,如果未能解决你的问题,请参考以下文章

c++中的左值和右值,右值引用到底是啥?关于引用这一节看得很迷糊。

左值和右值的区别

通过引用传递 - 左值和向量

具有适用于左值和右值的引用参数的 C++ 函数

[C++11]右值和右值引用

重新理解C11的右值引用