29

我已经看到它说,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);
4

1 回答 1

35

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

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

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

相同的问题std::string

MyTypeMyType数据成员为std::vector和/或时,同样的问题std::string

只有 2 次您需要考虑使用交换实现复制分配:

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

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

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

  1. 无例外交换。
  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<big_legacy_type>在哪里:

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放入 avector将需要 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以两种方式编写了 的复制赋值运算符:

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

该测试构造了一些随机大小vector<int>的 0 到 1000 之间的 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 一半的时间内具有足够的容量。如果我们把它带到任何一个极端:

  1. lhs 一直都有足够的容量。
  2. lhs 在任何时候都没有足够的容量。

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

想要速度?措施。

于 2013-08-18T21:20:27.333 回答