std::vector、默认构造函数、C++11 和破坏性变更

54

我今天遇到了一个非常微妙的问题,希望听听您的意见。

考虑以下普通的共享主体习语类:

struct S
{
    S() : p_impl(new impl) {}
private:
    struct impl;
    boost::shared_ptr<impl> p_impl;
};

当你尝试以以下方式将它们放入向量中时,就会出现有趣的情况:

std::vector<S> v(42);

现在,至少在MSVC 8中,v 中的所有元素共享相同的 impl 成员。实际上,导致这种情况的是 vector 构造函数:

template <typename T, typename A = ...>
class vector
{
    vector(size_t n, const T& x = T(), const A& a = A());
    ...
};

在幕后,只有一个S对象被默认构造,vectorn个元素从它复制而来。现在,使用C++11,有右值引用。因此无法像这样工作。如果一个vector被构造为:
std::vector<S> v(42);

最有可能的情况是,实现会选择默认构造 vector 内部的 n 个对象,因为复制构造函数可能不可用。在这种情况下这将是一个破坏性的变化。

我的问题是:

  1. C++03标准是否要求 std::vector 必须定义如上所述的构造函数,即带有默认参数?特别地,是否保证向量对象的条目被复制而不是默认构造?
  2. C++11标准对此同一点说了什么?
  3. 我认为这可能会导致C++03和C+11之间的破坏性变化。这个问题已经被调查过了吗?解决了吗?

注:请不要评论上面类 S 的默认构造函数。这样做或者实现某种形式的延迟构造。


2
这真是太令人震惊了。在C++代码中编译,但又给它赋予了C++0X的新含义,这是我所能想到的最糟糕的情况。我很想像当初放弃DOS 4或Windows ME一样,直接跳过这个问题。 - 6502
3
那是威胁吗?你是在把C++0x和Windows ME相比较吗? - jalf
5
毫无不敬,但听到一个名叫"6502"的人说这话有点有趣... - Potatoswatter
2个回答

46
C++03标准是否规定了std::vector必须有如上所述的构造函数,即带有默认参数?特别地,是否保证向量对象的条目被复制而不是默认构造?
是的,指定的行为是将x复制n次,以使容器初始化为包含n个元素,这些元素都是x的副本。
C++11标准对此问题有何规定?
在C++11中,此构造函数已被分成两个构造函数。
vector(size_type n, const T& x, const Allocator& = Allocator()); // (1)
explicit vector(size_type n);                                    // (2)

除了第二个参数不再有默认值之外,(1) 与 C++03 中的工作方式相同:将 x 复制 n 次。

在没有 x 的默认参数的情况下,(2) 被添加。此构造函数对容器中的 n 个元素进行值初始化。不会进行复制。

如果需要旧的行为,可以通过向构造函数调用提供第二个参数来确保调用 (1)

std::vector<S> v(42, S());
我认为这可能是C++03与C++11之间的一个破坏性变化。您的示例确实证明了这一点。关于这个问题是否已经被调查并解决,我不确定,因为我不是C++标准化委员会的成员(而且我没有特别关注邮件中与库有关的论文)。

1
@6502:这个更改是一个故意的决定,而不是疏忽(我认为疏忽地添加构造函数重载会很困难:-P)。在“当分配时它们也改变了吗?”方面,我不太确定你在问什么。由于移动构造函数和移动赋值运算符被引入到语言中,一些东西已经发生了变化,但我不确定这会如何特别影响算法。(此外,我并不完全熟悉去年底“隐式移动必须消失”的争论的最终决定是什么...)。 - James McNellis
9
@Alexandre:那就不是了。还有几种情况他们会破坏向后兼容性。只要能避免或得不偿失时,他们会尽量不这样做。但完全的、百分之百的向后兼容性已经多年没有被考虑过了。 - jalf
1
@Steve:添加关键词通常不会“悄无声息地”破坏事物。我同意@6502等人对此提出的质疑。 - Nemo
1
在 S.O. 上有很多关于为什么会发生复制构造的问题,通常表现为“为什么我的向量比数组慢得多?”。旧行为对于 C.O.W. / 引用计数对象是有意义的,但新行为在一般情况下更直观。 - Tony Delroy
3
@Nemo:当然,我在回应Alexandre C的话,“语言要么向后兼容,要么不兼容”。它是不兼容的,而且非常明显。如果他想使用三分法,“语言要么向后兼容;否则就不兼容;或者C++03中的每个格式正确的程序都会在C++0x中保留C++03所保证的所有行为,否则在C++0x中是不正常的”,那么他可以使用这个三分法来代替二分法。仍然是“不兼容”,只是不那么明显了,我同意不能有第三种情况有点遗憾。 - Steve Jessop
显示剩余8条评论

-3

我认为你描述的用例的解决方案不是最优的,也不完整,这就是为什么你在升级到C++11时遇到了问题。

C++总是关注语义,当你在C++中编写程序时,最好理解你的语义。所以在你的情况下,你希望创建N个对象,但在你不改变它们的情况下,你希望它们共享同一内存以进行优化。这是个好主意,但如何实现呢: 1)复制构造函数。 2)静态实现+复制构造函数。 你考虑过这两种解决方案吗?

考虑一下,如果你需要M个N个对象的向量,如果你选择第一种方案,共享内存会被分配多少次?是M次,但如果我们想要创建包含MxN个对象的向量,为什么要分配M次内存呢?

因此,在这里正确的实现是默认指向静态内存,并且只有在对象更改时才分配内存。在这种情况下,分配M个N个对象的向量将给你... 1个“共享”内存分配。

在你的情况下,你滥用了复制构造函数,违反了正确的语义,这是: 1)不明显 2)不优化 现在你必须付出代价。


9
你可以完全不同意帖子中的设计问题,但问题所涉及的程序在旧标准和新标准下都是有效且格式良好的。问题是关于在两种标准下是否为相同的程序,而不是关于设计决策的。 - André Caron

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