为什么C++11从std::vector的fill构造函数的原型中删除了默认值?

43
在C++98中,std::vector的填充构造函数原型有一个初始化器的默认值。
explicit vector (size_type n, const value_type& val = value_type(),
                 const allocator_type& alloc = allocator_type());

C++11使用两个原型。

explicit vector (size_type n);
         vector (size_type n, const value_type& val,
                 const allocator_type& alloc = allocator_type());

(在C++14中,填充构造函数又一次发生了变化,但这不是本问题的重点。)

一个参考链接在这里

C++11为什么弃用了默认初始化值value_type()

顺便说一下,我尝试使用clang++ -std=c++11编译以下代码,但它报错了,这意味着值类型仍然需要像S() {}那样有一个默认构造函数,即支持默认构造。

#include <vector>

struct S {
    int k;
    S(int k) : k(k) {} // intentionally remove the synthesized default constructor
};

int main() {
    std::vector<S> s(5); // error: no matching constructor
}

3
在C++11之前,你在最后展示的那个例子也不会起作用,因为S没有默认构造函数。而且C++11并没有废弃默认值,那个单一的构造函数被替换成了另外两个构造函数。 - Praetorian
5
默认值很危险。 - Jesper Juhl
@Praetorian 是的,我应该说C++11 删除了 构造函数原型中的默认值。 - Leedehai
2
@ Jesper Juhl 你需要详细说明一下。 - Nir Friedman
2个回答

51

C++98会复制一个原型对象n次。默认情况下,原型是一个默认构造的对象。

C++11版本会构造n个默认构造的对象。

这消除了n次复制,并用n个默认构造替换它。此外,它避免了构造原型。

假设你的类看起来像这样:

struct bulky {
  std::vector<int> v;
  bulky():v(1000) {} // 1000 ints
  bulky(bulky const&)=default;
  bulky& operator=(bulky const&)=default;

  // in C++11, avoid ever having an empty vector to maintain
  // invariants:
  bulky(bulky&& o):bulky() {
    std::swap(v, o.v);
  }
  bulky& operator=(bulky&& o) {
    std::swap(v,o.v);
    return *this;
  }
};

这是一个类,它始终拥有1000个int的缓冲区。

如果我们创建一个bulky的向量:

std::vector<bulky> v(2);

在C++98中,它分配了3次1000个整数。而在C++11中,只分配了2次1000个整数。

此外,C++98版本要求类型可复制。C++11中存在非可复制的类型,例如std::unique_ptr<T>,使用C++98签名无法生成默认构造的唯一指针vector。而C++11签名则没有这个问题。

std::vector<std::unique_ptr<int>> v(100);

如果我们仍在使用C++98版本,则上述方法将无法运行。


2
我可能会撤回这个解释,因为正确性胜过效率:D 但无论如何,这是对一个好问题的好回答。 - SergeyA
@SergeyA 我不确定何时从向量中放松对传递给向量的类型的要求,并改为与使用的确切方法耦合。效率差异在C++11中显然出现了;我不确定在技术上是否放松了要求。因此,我详细说明了我所知道的情况,然后提到在C++11/14的某个地方,它也导致了对类型的放松要求(在实践中,在C++11中,我不确定标准是否在C++14之前就放松了要求,因为我记不清了)。 - Yakk - Adam Nevraumont
1
@tobi303 原型存在于堆栈中,作为参数。对象存在于由向量管理的内存中。向量不管理参数,因此它不能是向量中的第一个对象。C++具有值语义,该对象具有明确定位。在C++11中,它们可以将其作为rvalue或lvalue参数,并从中移动,但是我的疯狂的“笨重”类型即使被移动也永远不会为空,所以这也没有帮助。 - Yakk - Adam Nevraumont
@Yakk 是的,有道理。我太专注于没有传递参数并使用默认的 value_type() 的情况,忘记了它也是一个参数。 - 463035818_is_not_a_number
1
@Yakk,在C++11中这一点变得更为宽松了。C++98中的explicit vector(size_type n, const T& value = T(),const Allocator& = Allocator());已经被拆分成了两个:explicit vector(size_type n);vector(size_type n, const T& value, const Allocator& = Allocator());。基本上,在C++98中,将元素放入向量的唯一方法是复制它们。 - Nevin
显示剩余2条评论

47
构造函数被分成两个的原因是为了支持“移动唯一”类型,例如unique_ptr<T>
这个构造函数:
vector(size_type n, const T& value, const Allocator& = Allocator());

需要 T 是可复制构造的,因为必须从 value 复制 nT 来填充 vector

此构造函数:

explicit vector(size_type n, const Allocator& = Allocator());

不需要 T 是可复制构造的,只需要是默认构造。

后一个构造函数可以与 unique_ptr<T> 一起使用:

std::vector<std::unique_ptr<int>> s(5);

前者的构造函数不支持此操作。这是导致更改的提案: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1858.html#23.2.4.1%20-%20vector%20constructors,%20copy,%20and%20assignment。这篇论文有一些理由,虽然有点简洁: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1771.html。顺便说一下,resize:
void resize(size_type sz, T c = T());

被分割成了:

void resize(size_type sz);
void resize(size_type sz, const T& c);

基于同样的原因,第一个需要默认构造但不要求可复制(以支持默认构造移动类型),而第二个需要可复制。

这些更改并非完全向后兼容。对于某些类型(例如引用计数智能指针),从默认构造对象进行复制构造与默认构造不同。然而,支持仅限移动类型的好处被认为值得付出此API变更的代价。


好观点。unique_ptr<T> 是一个非常好的不可复制类的例子。不幸的是,只能有一个被接受的答案。 - Leedehai
3
这似乎是真正的答案,因为它解释了为什么要这样做(Yakk目前被接受的答案也提到了,但只是顺便提及)。 - jamesdlin
他们不能只是使用 enable_if is_copy_constructible 吗? - user541686
4
@Mehrdad: 那样是可以做到的。我认为C++11中container<T>(n)的行为通常更可取,如果确实需要C++98的行为,则可以使用语法container<T>(n, T{})。在绝大多数情况下,没有语义上的区别(大多数用例不会改变)。存在一定风险,但收益更大。回顾起来,我认为这个赌注成功了。 - Howard Hinnant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接