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

Does the C++ standard specify STL implementation details for the compiler?

在写 问题的答案时,我遇到了一个有趣的情况 - 该问题演示了这样一种情况,即有人想将 class 放入 STL 容器中,但未能这样做,因为缺少副本 constructor/move constructor/assignment 运算符。在这种特殊情况下,错误是由 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 错误?

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

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

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

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

最重要的是,自 c++11 起,std::vector<>可以 存储不可复制的类型。 (example) Let's take a look at cppreference.

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

T must meet the requirements of CopyAssignable and CopyConstructible.

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

The requirements that are imposed on the elements depend on the actual operations performed on the container. Generally, it is required that element type is a complete type and meets the requirements of Erasable, but many member functions impose stricter requirements.

.. Erasable 是:

The type T is Erasable from the Container X if, given

A the allocator type defined as X::allocator_type

m the lvalue of type A obtained from X::get_allocator()

p the pointer of type T* prepared by the container

the following expression is well-formed:

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

并查看 std::vector::resize() reference 的 "Type requirements":

T must meet the requirements of MoveInsertable and DefaultInsertable in order to use overload (1).

所以 T 不需要是可复制的 - 它只需要是可破坏的、可移动的和默认可构造的。

此外,从c++14开始,完整类型的限制被移除。

The requirements that are imposed on the elements depend on the actual operations performed on the container. Generally, it is required that element type meets the requirements of Erasable, but many member functions impose stricter requirements. This container (but not its members) can be instantiated with an incomplete element type if the allocator satisfies the allocator completeness requirements.

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


C++11 标准论文N3337

void resize(size_type sz);

Effects: If sz <= size(), equivalent to erase(begin() + sz, end());. If size() < sz, appends sz - size() value-initialized elements to the sequence.

Requires: T shall be CopyInsertable into *this.

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

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

Visual C++ 2012 无法 auto-generate the move constructor and the move assignment operator. A defect that will only be fixed 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;
}

正如 , the standard actually does not require a move assignment operator here. The relevant concepts for vector<T>::resize() are MoveInsertable and DefaultInsertable 所详细解释的那样,您的初始实现只需使用移动构造函数就可以满足这一要求。

VC的实现也需要这里的移动分配是一个不同的缺陷,它已经在 VS2013 中修复。

感谢 and 在这件事上的深刻贡献。

VS2012 是一个具有一些 C++11 特性的 C++ 编译器。称它为 C++11 编译器有点牵强。

它的标准库非常C++03。它对移动语义的支持是最小的。

到 VS2015,编译器仍然是具有一些 C++11 功能的 C++11,但它对移动语义的支持要好得多。

VS2015 仍然缺乏完整的 C++11 constexpr 支持并且具有不完整的 SFINAE 支持(他们称之为 "expression SFINAE")和一些连锁库故障。它在非静态数据成员初始化器、初始化器列表、属性、通用字符名称、一些并发细节方面也有缺陷,并且它的预处理器不兼容。 This is extracted from their own blog.

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

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

您遇到的问题是您的编译器不是完整的 C++11 编译器,因此您的代码无法运行。

在这种情况下,C++11 标准也存在缺陷。缺陷报告通常由编译器修复并由编译器折叠到 "C++11 compiling mode" 中,并被合并到下一个标准中。有问题的缺陷非常明显,基本上每个实际实现 C++11 标准的人都忽略了这个缺陷。


C++ 标准要求某些可观察的行为。通常,这些要求将编译器编写者限制在某些狭窄的实现 space(有微小的变化),假设实现质量不错。

同时,C++标准留下了很大的自由度。 C++ 向量的迭代器类型可以是标准下的原始指针,或者在使用不当时会产生额外错误的引用计数智能索引器,或者完全是其他东西。编译器可以使用这种自由来让他们的调试版本进行额外的错误检查(为程序员捕获未定义的行为),或者使用这种自由来尝试不同的技巧来获得额外的性能(一个向量将其大小和容量存储在分配的缓冲区可以更小来存储,通常当您请求 size/capacity 时,您无论如何都会很快访问数据)。

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

通常会写一些参考实现,分析其局限性和复杂性界限,并提出这些作为限制。有时部分 "looser" 比参考实现所需的要少,这为编译器或库编写者提供了自由。

例如,有人抱怨说 C++11 中的无序映射类型受到标准的过度约束,阻碍了可能允许更有效实施的创新。如果对所述容器施加的限制更少,不同的供应商可以进行试验,并且可能已经融合了更快的容器而不是当前的设计。

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