【问题标题】:Why the push_back signature is void push_back (const value_type& val) not void push_back (value_type val)? [duplicate]为什么 push_back 签名是 void push_back (const value_type& val) 而不是 void push_back (value_type val)? [复制]
【发布时间】:2014-07-11 00:56:35
【问题描述】:

为什么push_back的函数签名如下?

void push_back (const value_type& val);

传递的值被复制到容器中,为什么不直接复制到参数列表中呢?

void push_back (value_type val);

【问题讨论】:

  • 为了避免制作另一个对象的副本。
  • 您所说的在emplace_back 中以正确的方式完成。但是,push_backemplace_back 都需要针对不同的情况和用法。
  • 另外,在设计这个接口时,C++ 无法东西从 destined-to-die 对象中移动,这意味着你建议的接口通常会导致在要制作的值的两个副本中(除非有任何编译器优化。)
  • 请注意,在 C++11 中,还有一个函数签名 void push_back (value_type&& val);,这允许编译器在传入 rvalue 时完全避免复制。

标签: c++ stl


【解决方案1】:

存储可移动和可复制的类

假设你有这个课程:

class Data {
 public:
  Data() { }
  Data(const Data& data)            { std::cout << "  copy constructor\n";} 
  Data(Data&& data)                 { std::cout << "  move constructor\n";}
  Data& operator=(const Data& data) { std::cout << "  copy assignment\n"; return *this;}
  Data& operator=(Data&& data)      { std::cout << "  move assignment\n"; return *this;}  
};

注意,一个好的 C++11 编译器 should define all these functions 适合您 (Visual Studio doesn't) 但我在此处定义它们以用于调试输出。

现在,如果您想编写一个类来存储其中一个类,我可能会像您建议的那样使用按值传递:

class DataStore {
  Data data_;
 public: 
  void setData(Data data) { data_ = std::move(data); }
};

我正在利用C++11 move semantics 将值移动到所需位置。然后我可以像这样使用DataStore

  Data d;   
  DataStore ds;
  
  std::cout << "DataStore test:\n";
  ds.setData(d);
  
  std::cout << "DataStore test with rvalue:\n";
  ds.setData(Data{});
  
  Data d2;
  std::cout << "DataStore test with move:\n";
  ds.setData(std::move(d2));

输出如下:

DataStore test:
  copy constructor
  move assignment
DataStore test with rvalue:
  move assignment
DataStore test with move:
  move constructor
  move assignment

这很好。我在上次测试中有两个动作可能不是最佳的,但动作通常很便宜,所以我可以忍受。为了使其更优化,我们需要重载 setData 函数,我们稍后会这样做,但这可能是过早的优化。

存储一个不可移动的类

但现在假设我们有一个可复制但不可移动的类:

class UnmovableData {
 public:
  UnmovableData() { }
  UnmovableData(const UnmovableData& data) { std::cout << "  copy constructor\n";}
  UnmovableData& operator=(const UnmovableData& data) { std::cout << "  copy assignment\n"; return *this;}  
};

在 C++11 之前,所有类都是不可移动的,因此希望今天能在野外找到很多类。如果我需要编写一个类来存储它,我不能利用移动语义,所以我可能会写这样的东西:

class UnmovableDataStore {
  UnmovableData data_;
 public:
  void setData(const UnmovableData& data) { data_ = data; }
};

并通过引用到常量传递。当我使用它时:

  std::cout << "UnmovableDataStore test:\n";
  UnmovableData umd;
  UnmovableDataStore umds;
  umds.setData(umd);

我得到了输出:

UnmovableDataStore test:
  copy assignment

如您所愿,只有一份副本。

存储不可复制的类

你也可以有一个可移动但不可复制的类:

class UncopyableData {
 public:
  UncopyableData() { } 
  UncopyableData(UncopyableData&& data) { std::cout << "  move constructor\n";}
  UncopyableData& operator=(UncopyableData&& data) { std::cout << "  move assignment\n"; return *this;}    
};

std::unique_ptr 是可移动但不可复制的类的示例。在这种情况下,我可能会编写一个类来存储它:

class UncopyableDataStore {
  UncopyableData data_;
 public:
  void setData(UncopyableData&& data) { data_ = std::move(data); }
};

我经过rvalue reference 并像这样使用它:

  std::cout << "UncopyableDataStore test:\n";
  UncopyableData ucd;
  UncopyableDataStore ucds;
  ucds.setData(std::move(ucd));

输出如下:

UncopyableDataStore test:
  move assignment

请注意,我们现在只有一个好的动作。

通用容器

然而,STL 容器需要是通用的,它们需要与所有类型的类一起工作并尽可能优化。如果你真的需要上面数据存储的通用实现,它可能看起来像这样:

template<class D>
class GenericDataStore {
  D data_;
 public:
  void setData(const D& data) { data_ = data; }
  void setData(D&& data) { data_ = std::move(data); }   
};

通过这种方式,无论我们使用不可复制或不可移动的类,我们都可以获得最佳性能,但我们必须至少有两个 setData 方法的重载,这可能会引入重复代码。用法:

  std::cout << "GenericDataStore<Data> test:\n";
  Data d3;
  GenericDataStore<Data> gds;
  gds.setData(d3);
  
  std::cout << "GenericDataStore<UnmovableData> test:\n";
  UnmovableData umd2;
  GenericDataStore<UnmovableData> gds3;
  gds3.setData(umd2); 
  
  std::cout << "GenericDataStore<UncopyableData> test:\n";
  UncopyableData ucd2;
  GenericDataStore<UncopyableData> gds2;
  gds2.setData(std::move(ucd2));

输出:

GenericDataStore<Data> test:
  copy assignment
GenericDataStore<UnmovableData> test:
  copy assignment
GenericDataStore<UncopyableData> test:
  move assignment

Live demo.希望对您有所帮助。

【讨论】:

    【解决方案2】:

    以下是使用这些接口中的每一个实现时将push_back 极其简化为向量的样子:

    // by reference
    void push_back (value_type const & val)
    {
        // Copy val into its designated place.
        new (m_data_ptr + m_len++) value_type (val);
    }
    
    // by value
    void push_back (value_type val)
    {
        // Copy val into its designated place.
        // In C++11, this copy may not happen if value_type is movable. But that's
        // not always the case. (you have to use std::move too.)
        new (m_data_ptr + m_len++) value_type (val);
    }
    

    它们看起来一样,不是吗?

    问题是当您尝试调用它们时,特别是传值版​​本:

    string s;
    ...
    v.push_back (s);
    

    如果push_back 接受它的参数通过引用(即value_type &amp; val),那么对现有对象s 的引用将被传递到函数中,并且此处不进行任何复制。当然,我们在函数内部仍然需要一个副本,但这有点必要。

    但是,如果写入push_back按值 获取其参数(即value_type val),则将在调用站点复制s 字符串到堆栈并进入将被命名为val 的参数。这里,val 不是对字符串的引用,它一个字符串,它必须来自某个地方。这个额外的副本是促使 STL 和最明智的 C++ 库的设计者在许多情况下采用传递引用作为首选的原因(如果您想知道,const 可以告诉调用者现在这个函数可以修改它的珍贵对象,因为它的引用被赋予了这个函数,它不会!)

    顺便说一下,这个讨论主要适用于 C++98(即旧的 C++)。当前的 C++ 具有 Rvalue 引用以及移动和完美的转发,这提供了更多的接口选项和更清洁、更精确和更多的机会高效的接口/实现,但也使这个主题变得更加复杂。

    在 C++11 中,向量和其他容器上有两个 push_back 重载(以及一个新成员 emplace_back)。

    push_backs 是:

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

    第二个是您所建议的正确版本(即它不会对编译器产生歧义。)它允许实现将值移出该右值引用,并让编译器生成代码来调用如果合适的话,更快的版本。

    出于向后兼容的原因(可能还有其他一些次要原因),旧的 push_back 签名无法从 C++ 中删除。

    【讨论】:

    • +1 用于右值引用
    【解决方案3】:

    几个原因:

    • 已为相关班级制作了一份副本。如果您没有const value_type&amp; val,您将强制创建另一个副本。通过引用 (value_type&amp;) 传递它可以帮助您做到这一点。
    • 这也告诉编译器val不能以任何方式修改。这是通过将其设为“const”来完成的
    • 当然,一旦你复制了,你就可以修改它,但是val不能以任何方式修改,函数声明保证

    【讨论】:

      【解决方案4】:

      答案是避免再次复制。看看这个简单的例子,它说明了使用 value_typeconst value_type&amp; 之间的区别。

      #include <iostream>
      using namespace std;
      
      struct A
      {
         A() {}
      
         A(A const& copy)
         {
            cout <<  "Came to A::A(A const& copy)\n";
         }
      
         void print() const
         {
            cout << "Came to A:print()\n";
         }
      
      };
      
      void foo(A const& a)
      {
         A copy = a;
         copy.print();
      }
      
      
      void bar(A a)
      {
         A copy = a;
         copy.print();
      }
      
      int main()
      {
         A a;
         foo(a);
         bar(a);
      }
      

      运行程序的输出:

      来到 A::A(A const& copy) 来到 A:print() 来到 A::A(A const& copy) 来到 A::A(A const& copy) 来到 A:print()

      请注意由于对bar 的调用而对复制构造函数的额外调用。对于某些对象,在执行数百万次操作时,额外的复制构造和相应的销毁可能非常昂贵。

      【讨论】:

      • 这只是因为您的代码正在制作std::vector::push_back 不需要制作的不必要的副本。我认为 OP 的论点是,既然值无论如何都会被复制到容器中,为什么不通过复制将其传递给push_back,然后将其移动到容器存储中。您的回答没有解决这个(非常有效的)观点。
      • 为什么A copy = a;bar 里面?为什么不直接使用a
      • @KonradRudolph move 操作在 C++03 中不可用。我的函数所做的正是std::vectorstd::list 等容器所做的——制作对象的副本并存储副本。
      • @ChrisDrew 希望我之前的评论回答了你的问题。
      • @RSahu std::vector 容器在 C++98 之前不可用。 ;-) 然而,pre-C++98 和 C++03 都不再相关了。您的答案(仅)在历史上是正确的——但是为什么 C++11 没有改变呢?原因要微妙得多。
      猜你喜欢
      • 2013-08-18
      • 2012-06-09
      • 2021-03-11
      • 2016-05-17
      • 2013-04-16
      • 2010-10-01
      • 1970-01-01
      • 2014-07-06
      • 1970-01-01
      相关资源
      最近更新 更多