为什么push_back签名是void push_back(const value_type& val)不是void push_back(value_type val)?

时间:2014-05-22 06:20:40

标签: c++ stl

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

void push_back (const value_type& val);

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

void push_back (value_type val);

4 个答案:

答案 0 :(得分:2)

答案是避免制作另一份副本。看一下这个简单的例子,它说明了使用value_typeconst value_type&之间的区别。

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

运行程序的输出:

Came to A::A(A const& copy)
Came to A:print()
Came to A::A(A const& copy)
Came to A::A(A const& copy)
Came to A:print()

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

答案 1 :(得分:1)

这里有一个非常简化的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 & val),则会将对现有对象s的引用传递给该函数,并且不会复制在这里制作当然,我们仍然需要在函数内部使用一个副本,但这是必要的。

但是,如果编写push_back以按值获取其参数 (即value_type val),则会在s字符串处创建一个副本调用站点,进入堆栈并进入名为val的参数。这里,val不是对字符串的引用,字符串,它必须来自某个地方。这个额外的副本驱使STL的设计者和最敏感的C ++库采用pass-by-reference作为许多情况下的首选(如果你想知道,const可以告诉你现在这个函数可以修改其珍贵对象的调用者,因为对函数的引用被赋予了函数,它不会被激活!)

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

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

push_back是:

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

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

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

答案 2 :(得分: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)但我在这里为调试输出定义它们。

现在,如果你想编写一个类来存储其中一个类,我可能会像你建议的那样使用pass-by-value:

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

并通过引用传递给const。当我使用它时:

  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.希望有所帮助。

答案 3 :(得分:0)

几个原因:

  • 已经向该课程提供了一份副本。如果您没有const value_type& val,则会强制另一份副本。通过引用(value_type&)传递它可以帮助您做到这一点。
  • 这也告诉编译器无法以任何方式修改val。这是通过使它成为“const”
  • 来完成的
  • 当然,一旦你制作副本,你就可以修改它,但val不能以任何方式修改,而且函数声明是保证