视觉 C++标准是否指定编译器的 STL 实现详细信息

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

本文关键字:STL 实现 详细信息 编译器 C++ 标准 是否 视觉      更新时间:2023-10-16

在写这个问题的答案时,我遇到了一个有趣的情况 - 这个问题演示了一个场景,即人们想要将类放在 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错误?

>Visual C++ 2012 无法自动生成移动构造函数和移动赋值运算符。仅在即将推出的 2015 版本中修复的缺陷。

您可以通过向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 的回答所详细解释的那样,该标准实际上不需要这里的移动分配运算符。vector<T>::resize()的相关概念是 MoveInsertable 和 DefaultInsertable,只需使用 move 构造函数即可通过初始实现来满足这些概念。

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

感谢ikh和dyp在这件事上做出的有见地的贡献。

最重要的是,从 c++11 开始,std::vector<>可以存储不可复制的类型。(示例)让我们来看看 cpp 偏好。

在 c++11 之前,如您所知,T 应该是可复制的。

T 必须满足 CopyAssignable 和 CopyConstructable 的要求。

但是,在 c++11 中,要求完全改变了。

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

..可擦除是:

类型 T 可从容器 X 中擦除,如果给定

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

mX::get_allocator()获得的 A 类型的左值

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

以下表达式格式正确:

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

看看 std::vector::resize() 引用的"类型要求":

T 必须满足 MoveInsertable 和 DefaultInsertable 的要求才能使用重载 (1)。

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

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

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

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


C++11标准纸N3337说

空隙调整大小(size_type sz);

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

要求:T 应可复制插入到 *this 中。

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

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

VS2012是一个C++编译器,具有一些C++11功能。 称其为 C++11 编译器有点牵强。

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

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

VS2015仍然缺乏完整的C++11 constexpr支持,并且具有不完整的SFINAE支持(他们称之为"表达式SFINAE")和一些连锁库故障。 它还在非静态数据成员初始值设定项、初始值设定项列表、属性、通用字符名称、某些并发详细信息方面存在缺陷,并且其预处理器不合规。 这是从他们自己的博客中提取的。

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

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

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

在这种情况下,C++11标准也存在缺陷。 缺陷报告通常由编译器修复,并由编译器折叠成"C++11编译模式",并被纳入下一个标准。 所讨论的缺陷非常明显,基本上每个实际实施C++11标准的人都忽略了这个缺陷。


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

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

限制通常与数据生存期和复杂性边界有关。

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

例如,有人抱怨 C++11 中的无序映射类型受到标准的过度约束,并阻止了可以允许更高效实现的创新。 如果对上述容器施加的约束较少,则可以尝试不同的供应商,并且可能会收敛更快的容器而不是当前的设计。

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

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

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

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

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