C ++标准是否为编译器指定了STL实现细节?

时间:2015-06-19 10:26:44

标签: c++ visual-c++ gcc stl clang

在写this问题的答案时,我遇到了一个有趣的情况 - 问题演示了一个人想要将一个类放在STL容器中但由于缺少复制构造函数/移动而无法这样做的情况构造函数/赋值运算符。在这种特殊情况下,错误由std::vector::resize触发。我做了一个快速片段作为解决方案,并看到另一个答案,提供了一个移动构造函数,而不是像我一样提供赋值运算符和复制构造函数。什么是有趣的,其他答案没有在VS 2012中编译,而clang / gcc对这两种方法都很满意。

首先:

// Clang and gcc are happy with this one, VS 2012 is not
#include <memory>
#include <vector>

class FooImpl {};

class Foo
{
    std::unique_ptr<FooImpl> myImpl;
public:
    Foo( Foo&& f ) : myImpl( std::move( f.myImpl ) ) {}
    Foo(){}
    ~Foo(){}
};

int main() {
    std::vector<Foo> testVec;
    testVec.resize(10);
    return 0;
}

第二

// Clang/gcc/VS2012 are all happy with this
#include <memory>
#include <vector>

using namespace std;
class FooImpl {};

class Foo
{
    unique_ptr<FooImpl> myImpl;
public:
    Foo()
    {
    }
    ~Foo()
    {
    }
    Foo(const Foo& foo)
    {
        // What to do with the pointer?
    }
    Foo& operator= (const Foo& foo)
    {
        if (this != &foo)
        {
            // What to do with the pointer?
        }
        return *this;
    }
};

int main(int argc, char** argv)
{
    vector<Foo> testVec;
    testVec.resize(10);
    return 0;
}

为了理解发生了什么,我查看了VS 2012中的STL源代码,发现它确实调用了移动赋值运算符,这就是为什么我的样本有效(我没有Linux机器可以访问以了解将要发生的事情)在clang / gcc中,而另一个没有,因为它只有移动复制构造函数。

因此,这创建了以下问题 - 编译器可以自由决定如何实现STL方法(在本例中为std::vector::resize),因为根本不同的实现可能会导致不可移植的代码?或者这只是一个VS 2012错误?

4 个答案:

答案 0 :(得分:6)

Visual C ++ 2012无法auto-generate the move constructor and the move assignment operator。只会修复in the upcoming 2015 version的缺陷。

您可以通过向Foo添加显式移动赋值运算符来编译第一个示例:

#include <memory>
#include <vector>

class FooImpl {};

class Foo
{
    std::unique_ptr<FooImpl> myImpl;
public:
    Foo( Foo&& f ) : myImpl( std::move( f.myImpl ) ) {}
    // this function was missing before:
    Foo& operator=( Foo&& f) { myImpl = std::move(f.myImpl); return *this; }
    Foo(){}
    ~Foo(){}
};

int main() {
    std::vector<Foo> testVec;
    testVec.resize(10);
    return 0;
}

正如ikh's answer详细解释的那样,标准实际上并不需要移动赋值运算符。 vector<T>::resize()的相关概念是MoveInsertableDefaultInsertable,只需使用移动构造函数,您的初始实现就可以满足这些概念。

VC的实现还要求移动分配这一事实是不同的缺陷,已在VS2013中修复。

感谢ikhdyp在此问题上的深刻见解。

答案 1 :(得分:5)

最重要的是,由于c ++ 11,std::vector<> 可以存储不可复制的类型。 (example)让我们来看看cppreference

直到c ++ 11,如你所知,T应该是可复制的。

  

T必须符合CopyAssignable和CopyConstructible的要求。

然而,在c ++ 11中,要求完全改变了。

  

对元素施加的要求取决于对容器执行的实际操作。通常,要求元素类型是完整类型并满足可擦除的要求,但许多成员函数强加了更严格的要求。

.. Erasable是:

  

如果给定

,则类型T可以从容器X中删除      

A 分配器类型定义为X::allocator_type

     

m A

获取的X::get_allocator()类型的左值      

p 由容器准备的T*类型的指针

     

以下表达式格式正确:

std::allocator_traits<A>::destroy(m, p);

查看&#34;类型要求&#34; std::vector::resize() reference

  

T必须符合MoveInsertableDefaultInsertable的要求才能使用重载(1)。

所以T不需要是可复制的 - 它只需要可销毁,可移动和默认的构造。

此外,由于c ++ 14,删除了完整类型的限制。

  

对元素施加的要求取决于对容器执行的实际操作。通常,要求元素类型满足Erasable的要求,但许多成员函数强加了更严格的要求。 如果分配器满足分配器完整性要求,则可以使用不完整的元素类型实例化此容器(但不是其成员)。

因此,我认为这是因为VS2012的标准符合性差。它在最新的C ++上有一些缺陷(例如noexcept

C ++ 11标准论文N3337

  

void resize(size_type sz);

     

效果:如果sz <= size(),相当于erase(begin() + sz, end());。如果size() < sz,请附加   sz - size()将值初始化的元素添加到序列中。

     

要求:T应为CopyInsertable到* this。

因此,在严格的c ++ 11中,在这种情况下你不能使用std::vector::resize()。 (你可以使用std::vector

然而,it is a standard defect并在C ++ 14中修复。我想很多编译器都能很好地处理不可复制的类型,因为复制并不需要实现std::vector::resize()。虽然VS2012不起作用,但是因为VS2012的另一个错误是@ComicSansMS回答,而不是因为std::vector::resize()本身。

答案 2 :(得分:3)

VS2012是一个带有一些C ++ 11特性的C ++编译器。将它称为C ++ 11编译器有点紧张。

它的标准库非常C ++ 03。它对移动语义的支持很少。

通过VS2015,编译器仍然是带有一些C ++ 11特性的C ++ 11,但它对移动语义的支持要好得多。

VS2015仍然缺乏完整的C ++ 11 constexpr支持,并且SFINAE支持不完整(他们称之为#34;表达SFINAE&#34;)以及一些连锁库故障。它还具有非静态数据成员初始值设定项,初始化程序列表,属性,通用字符名称,某些并发细节以及其预处理程序不兼容的缺陷。 This is extracted from their own blog

与此同时,现代gcc和clang编译器已经完成了对C ++ 14的支持并且拥有广泛的C ++ 1z支持。 VS2015具有有限的C ++ 14功能支持。几乎所有的C ++ 1z支持都在实验分支中(这是公平的)。

所有3个编译器都在它们支持的功能之上存在错误。

您在这里遇到的是您的编译器不是完整的C ++ 11编译器,因此您的代码不起作用。

在这种情况下,C ++ 11标准也存在缺陷。缺陷报告通常由编译器修复并折叠成&#34; C ++ 11编译模式&#34;通过编译器,以及纳入下一个标准。有问题的缺陷是显而易见的,基本上每个实际执行C ++ 11标准的人都忽略了缺陷。

C ++标准要求某些可观察的行为。这些任务通常会将编译器编写者限制在某些狭窄的实现空间(具有微小的变化),假设具有相当好的实现质量。

与此同时,C ++标准留下了很多自由。 C ++向量中的迭代器类型可以是标准下的原始指针,也可以是引用计数智能索引器,如果使用不当或其他原因产生额外错误。编译器可以使用这种自由来使他们的调试版本具有额外的错误检查(捕获程序员的未定义行为),或者使用该自由来尝试可以授予额外性能的不同技巧(在分配时存储其大小和容量的向量)缓冲区可能更小,以便存储,通常当您要求大小/容量时,无论如何都会很快访问数据。)

这些限制通常围绕数据生命周期和复杂性限制。

通常会编写一些参考实现,分析其局限性和复杂性界限,并将其作为限制提出。有时候部件会被放置,而且会更松散。而不是参考实现所需要的,这为编译器或库编写者提供了自由。

作为一个例子,有人抱怨C ++ 11中的无序地图类型受到标准的过度约束,并阻止了可以实现更高效实施的创新。如果放在所述容器上的约束较少,不同的供应商可以进行试验,并且可能会聚合更快的容器而不是当前的设计。

缺点是标准库的修改很容易破坏二进制兼容性,因此如果稍后添加的约束排除了某些实现,编译器编写者和用户可能会非常恼火。

答案 3 :(得分:1)

C ++标准规定了T对几乎所有库容器函数的约束。

例如,在草案n4296中,[vector.capacity] / 13中定义的T std::vector::resize的约束是。

Requires: T shall be MoveInsertable and DefaultInsertable into *this.

我无法访问各种版本的C ++的最终标准进行比较,但我认为VS 2012在此示例中的C ++ 11支持中不符合。