何时重载通过引用(l值和r值)优先传递给值?

时间:2013-08-18 20:22:32

标签: c++ c++11 overloading assignment-operator copy-and-swap

我已经看到它说用operator=写一个相同类型by-value的参数作为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);

1 个答案:

答案 0 :(得分:29)

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

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

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

std::string的相同问题。

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

您只想考虑使用swap实现复制分配的次数:

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

  2. 您知道总是需要复制赋值运算符才能获得强大的异常安全保障。

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

    1. noexcept swap。
    2. noexcept移动赋值运算符。
    3. 例如:

      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;
      }
      

      现在有一些类型可以使用swap实现复制赋值。但是这些类型将是例外,而不是规则。

      在:

      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加入vector将需要2个副本而不是1个,即使capacity已足够。这将是一场灾难,表现明智。

      <强>更新

      这是一个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";
      }
      

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

      1. 隐式,相当于调用vector的复制赋值运算符。
      2. 使用复制/交换习语,暗示在宏SLOW_DOWN下。我考虑过将其命名为SLEEP_FOR_AWHILE,但如果您使用的是电池供电设备,这种方式实际上比睡眠语句更差。
      3. 测试构造一些0到1000之间随机大小的vector<int>,并为它们分配一百万次。它计算每一个,对时间求和,然后找到浮点纳秒的平均时间并打印出来。如果对高分辨率时钟的两次连续调用没有返回小于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没有足够的容量。
        3. 然后,默认复制分配相对于复制/交换习惯用法的性能优势从大约560%到0%不等。复制/交换习语永远不会更快,并且可能会显着变慢(对于此测试)。

          想要速度?测量