为什么QString和vector <unique_ptr <int>&gt;在这里出现不兼容?

时间:2015-12-07 20:23:43

标签: c++ qt vector unique-ptr qstring

我正在尝试编译一些代码,这简化为:

#include <memory>
#include <vector>
#include <QString>

class Category
{
    std::vector<std::unique_ptr<int>> data;
    QString name;
};

int main()
{
    std::vector<Category> categories;
    categories.emplace_back();
};

按原样编译,它导致g ++中出现以下错误,类似于clang ++:

In file included from /opt/gcc-4.8/include/c++/4.8.2/memory:64:0,
                 from test.cpp:1:
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_construct.h: In instantiation of ‘void std::_Construct(_T1*, _Args&& ...) [with _T1 = std::unique_ptr<int>; _Args = {const std::unique_ptr<int, std::default_delete<int> >&}]’:
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:75:53:   required from ‘static _ForwardIterator std::__uninitialized_copy<_TrivialValueTypes>::__uninit_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = __gnu_cxx::__normal_iterator<const std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; _ForwardIterator = std::unique_ptr<int>*; bool _TrivialValueTypes = false]’
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:117:41:   required from ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = __gnu_cxx::__normal_iterator<const std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; _ForwardIterator = std::unique_ptr<int>*]’
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:258:63:   required from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = __gnu_cxx::__normal_iterator<const std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; _ForwardIterator = std::unique_ptr<int>*; _Tp = std::unique_ptr<int>]’
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_vector.h:316:32:   required from ‘std::vector<_Tp, _Alloc>::vector(const std::vector<_Tp, _Alloc>&) [with _Tp = std::unique_ptr<int>; _Alloc = std::allocator<std::unique_ptr<int> >]’
test.cpp:5:7:   [ skipping 2 instantiation contexts, use -ftemplate-backtrace-limit=0 to disable ]
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:117:41:   required from ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = Category*; _ForwardIterator = Category*]’
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:258:63:   required from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = Category*; _ForwardIterator = Category*; _Tp = Category]’
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:281:69:   required from ‘_ForwardIterator std::__uninitialized_move_if_noexcept_a(_InputIterator, _InputIterator, _ForwardIterator, _Allocator&) [with _InputIterator = Category*; _ForwardIterator = Category*; _Allocator = std::allocator<Category>]’
/opt/gcc-4.8/include/c++/4.8.2/bits/vector.tcc:415:43:   required from ‘void std::vector<_Tp, _Alloc>::_M_emplace_back_aux(_Args&& ...) [with _Args = {}; _Tp = Category; _Alloc = std::allocator<Category>]’
/opt/gcc-4.8/include/c++/4.8.2/bits/vector.tcc:101:54:   required from ‘void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {}; _Tp = Category; _Alloc = std::allocator<Category>]’
test.cpp:14:29:   required from here
/opt/gcc-4.8/include/c++/4.8.2/bits/stl_construct.h:75:7: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]’
     { ::new(static_cast<void*>(__p)) _T1(std::forward<_Args>(__args)...); }
       ^
In file included from /opt/gcc-4.8/include/c++/4.8.2/memory:81:0,
                 from test.cpp:1:
/opt/gcc-4.8/include/c++/4.8.2/bits/unique_ptr.h:273:7: error: declared here
       unique_ptr(const unique_ptr&) = delete;
       ^
  • 如果我从name删除了Category成员,则编译正常。
  • 如果我只使用data unique_ptr<int>而不是指针向量,那么编译就可以了。
  • 如果我在Category中创建一个main()而不是创建一个向量并执行emplace_back(),那么它编译得很好。
  • 如果我将QString替换为std::string,则编译正常。

发生了什么事?是什么让这个代码格式不正确?这是g ++&amp;中错误的结果吗?铛++?

1 个答案:

答案 0 :(得分:4)

这里的关键问题是std::vector尝试为尽可能多的操作提供strong exception safety guarantee,但是,为了做到这一点,它需要元素类型的支持。对于push_backemplace_back和朋友,主要问题是如果需要重新分配会发生什么,因为现有元素需要复制/移动到新存储。

相关标准措辞见[23.3.6.5p1]:

  

备注:如果新大小大于旧容量,则会导致重新分配。如果没有重新分配,则所有迭代器和引用都会发生   在插入点保持有效之前。如果抛出异常   除了由复制构造函数,移动构造函数,赋值   运算符,或移动T或任何InputIterator的赋值运算符   操作没有效果。如果抛出异常   在末尾插入单个元素,TCopyInsertable或   is_nothrow_move_constructible<T>::valuetrue,没有   效果。否则,如果移动构造函数抛出异常   非CopyInsertable T,效果未指定。

({3}}的决议澄清了C ++ 11中的原始措辞。)

请注意,is_nothrow_move_constructible<T>::value == true并不一定意味着Tnoexcept移动构造函数;使用noexcept的{​​{1}}副本构造函数也可以。

这在实践中意味着,概念const T&实现通常会尝试为以下解决方案之一生成代码,以便将现有元素复制/移动到新存储中,降序的顺序(vector是元素类型,我们对这里的类类型感兴趣):

  • 如果T有一个可用的(现有的,未删除的,不含糊的,可访问的等)T移动构造函数,请使用它;在新存储中构造元素时不能抛出异常,因此不需要恢复到以前的状态。
  • 否则,如果noexcept有一个可用的复制构造函数T,那么需要noexcept,使用它;即使复制抛出异常,我们也可以恢复到之前的状态,因为原件仍在那里,未经修改。
  • 否则,如果const T&有一个可用的移动构造函数,可能会抛出异常,请使用它;但是,不能再提供强大的异常安全保证。
  • 否则,代码无法编译。

以上可以通过使用LWG 2252或类似的东西来实现。

让我们看看T在构造函数方面提供的内容。 None是显式声明的,因此隐式声明默认构造函数,复制构造函数和移动构造函数。

复制构造函数使用成员的相应复制构造函数:

  • Categorydatastd::vector的复制构造函数不能是vector(它通常需要分配新的内存),所以{{1无论noexcept有什么,复制构造函数都不能是Category
  • noexcept复制构造函数的定义调用QString的复制构造函数,它被明确删除,但这只会影响定义,只有在需要时才会实例化。重载解析只需要声明,因此std::vector<std::unique_ptr<int>>有一个隐式声明的复制构造函数,如果调用它将导致编译错误。

移动构造函数:

  • std::unique_ptr<int>有一个Category移动构造函数(请参阅下面的注释),因此std::vector不是问题。
  • noexcept的旧版本(在Qt 5.2之前):
    • 未显式声明移动构造函数(请参阅std::move_if_noexcept),因此,由于存在显式声明的复制构造函数,因此不会在所有中隐式声明移动构造函数
    • 隐式声明的data移动构造函数的定义将使用QString的复制构造函数,该构造函数接受Category,它可以绑定到rvalues(子对象的构造函数)使用重载分辨率选择。
    • 在这些旧版本中,QString的复制构造函数未指定为const QString&,因此QString的移动构造函数不能{ {1}}。
  • 从Qt 5.2开始,noexcept有一个显式声明的移动构造函数,它将由Category的移动构造函数使用。但是,在Qt 5.5之前,noexcept的移动构造函数不是QString,因此Category的移动构造函数不能是QString无论是。
  • 自Qt 5.5起,noexcept的移动构造函数被指定为Category,因此noexcept的移动构造函数也是QString

请注意,noexcept在所有情况下都有移动构造函数,但它可能不会移动Category,而可能不会移动noexcept

鉴于上述所有情况,我们可以看到Category在使用Qt 4时无法生成使用name移动构造函数的代码(OP&#39; s case),因为它不是noexcept。 (当然,在这种情况下,没有现有元素可以移动,但这是运行时决定; categories.emplace_back()必须包含处理一般情况的代码路径,并且代码路径必须编译。)因此,生成的代码调用Category的复制构造函数,这会导致编译错误。

解决方案是为noexcept提供移动构造函数并将其标记为emplace_back(否则无法提供帮助)。无论如何Category使用写时复制,因此在复制时不太可能抛出。

这样的事情应该有效:

Category

如果声明,这将获取noexcept的移动构造函数,否则使用复制构造函数(就像隐式声明的移动构造函数一样)。既然构造函数是用户声明的,那么也必须考虑赋值运算符。

问题中对子弹1,3和4的解释现在应该非常明确。子弹2(使QString只有一个class Category { std::vector<std::unique_ptr<int>> data; QString name; public: Category() = default; Category(const Category&) = default; Category(Category&& c) noexcept : data(std::move(c.data)), name(std::move(c.name)) { } // assignment operators }; )更有趣:

  • QString有一个已删除的复制构造函数;这导致data隐式声明的复制构造函数也被定义为已删除。
  • unique_ptr<int>移动构造函数仍然如上所述(在OP的情况下不是unique_ptr)。
  • 这意味着为Category生成的代码无法使用Category的复制构造函数,因此它必须使用移动构造函数,即使它可以抛出(请参阅上面的第一部分) )。代码编译,但它不再提供强大的异常安全保证。

注意:noexcept移动构造函数最近才在标准中被指定为emplace_back,在C ++ 14之后,由于采用了Praetorian's comment above进入工作草案。然而,实际上,自C ++ 0x的时间以来,libstdc ++和libc ++都为Category提供了vector移动构造函数;与标准规范相比,允许实现强化异常规范,这样就可以了。

libc ++实际上使用noexcept用于C ++ 14及更低版本,但是自C ++ 11([17.6.3.5]中的表28)以来,分配器必须不是移动和复制可构造的,所以&#39 ;对于符合标准的分配器而言是多余的。

注意(更新):关于强大的异常安全保证的讨论并不适用于2017版之前MSVC附带的标准库实现:包括Visual Studio 2015 Update 3在内,它总是试图移动,无论noexcept规范如何。

根据Stephan T. Lavavej的N4258,MSVC 2017中的实施已经过彻底改革,现在表现得如上所述。

除非另有说明,否则标准参考号为N4567工作草案。