为什么在这里QString和vector<unique_ptr<int>>不兼容?

5

我正在尝试编译一些代码,它可以简化为以下内容:

#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++等编译器生成以下错误:

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;
       ^
  • 如果我从Category中删除name成员,则可以编译通过。
  • 如果我将data只设置为单个unique_ptr<int>而不是指针的向量,则可以编译通过。
  • 如果我在main()中创建一个单独的Category而不是创建向量并执行emplace_back(),则可以编译通过。
  • 如果我将QString替换为std::string,则可以编译通过。

发生了什么?这段代码为何不符合规范?这是g++和clang++中的错误吗?


1
可能是因为 QString 定义了一个拷贝构造函数,但没有移动构造函数,这意味着它的移动构造函数被隐式定义为删除。然后 emplace_back 调用将尝试复制 unique_ptr,导致上述错误。 - Praetorian
根据这里的说明,QString的移动构造函数是在Qt5.2中引入的,您使用的是旧版本吗? - Praetorian
@Praetorian 我这里正在使用Qt4。 - Ruslan
我认为这就是你的问题。你需要为Category定义自己的移动构造函数。我认为QString是引用计数的,对吗?如果是的话,在你的移动构造函数中复制QString可能是可以接受的。 - Praetorian
1
实际上,即使文档没有提及,在2015年1月,移动构造函数被标记为noexcept,这很可能是它能够运行的原因(拷贝构造函数几天后也被标记为noexcept)。你可以尝试使用一个包含所有隐式/显式移动构造函数和noexcept规范是否存在的虚类,然后看看是什么使它正常工作。 - bogdan
显示剩余5条评论
1个回答

5
这里的关键问题是,std::vector 尽可能为许多操作提供 强异常安全保证,但是为了实现这一点,它需要元素类型的支持。对于 push_backemplace_back 和相关函数,主要问题在于如果需要重新分配空间,现有元素就需要被复制/移动到新的存储位置中会发生什么。

相关标准文本在 [23.3.6.5p1] 中。

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

(C++11中的原始措辞已通过LWG 2252的决议得到澄清。)

请注意,is_nothrow_move_constructible<T>::value == true并不一定意味着T具有一个noexcept移动构造函数;一个以const T&为参数的noexcept复制构造函数也可以。

实际上,这意味着,对于vector实现来说,概念上通常会尝试生成以下解决方案之一的代码,以将现有元素复制/移动到新存储中,按优先级递减(这里关注类类型T):

  • 如果类型 T 有可用的(存在且未删除、不含歧义、可访问等)noexcept 移动构造函数,则使用它;在新存储中构造元素时无法抛出异常,因此无需回滚到先前状态。
  • 否则,如果类型 T 有可用的拷贝构造函数(noexcept 或非 noexcept),接受一个 const T&,则使用该函数;即使复制过程中抛出异常,我们也可以回滚到先前状态,因为原始数据仍然存在且未修改。
  • 否则,如果类型 T 有可用的可能会抛出异常的移动构造函数,则使用该函数;但是,强异常安全性保证将不再提供。
  • 否则,代码将无法编译。

可以通过使用 std::move_if_noexcept 或类似方式来实现上述操作。


让我们看一下 Category 构造函数提供了什么。没有明确声明,因此默认构造函数、复制构造函数和移动构造函数都被隐式声明。
复制构造函数使用成员的相应复制构造函数:
- data 是一个 std::vector,而 vector 的复制构造函数不能是 noexcept(通常需要分配新内存),因此无论 QString 有什么,Category 的复制构造函数都不能是 noexcept。 - std::vector<std::unique_ptr<int>> 的定义调用了 std::unique_ptr<int> 的复制构造函数,但这只影响定义,只有在需要时才会实例化。重载决议只需要声明,因此 Category 有一个隐式声明的复制构造函数,如果调用会导致编译错误。
移动构造函数:
  • std::vector具有noexcept移动构造函数(见下面的注释),因此data不是问题。
  • 旧版本的QString(Qt 5.2之前):
    • 没有明确声明移动构造函数(请参见Praetorian's comment above),因此,由于已经明确声明了复制构造函数,移动构造函数将根本不会被隐式声明。
    • Category的隐式声明移动构造函数的定义将使用一个接受const QString&QString的复制构造函数,它可以绑定到右值(子对象的构造函数使用重载分辨率选择)。
    • 在这些旧版本中,QString的复制构造函数未指定为noexcept,因此Category的移动构造函数也不能是noexcept
  • 自Qt 5.2以来,QString具有明确声明的移动构造函数,该构造函数将由Category的移动构造函数使用。但是,在Qt 5.5之前,QString的移动构造函数不是noexcept,因此Category的移动构造函数也不能是noexcept
  • 自Qt 5.5以来,QString的移动构造函数被指定为noexcept,因此Category的移动构造函数也是noexcept
请注意,所有情况下Category都有移动构造函数,但它可能不会移动name,并且可能不是noexcept
考虑到上述所有内容,我们可以看出,在使用Qt 4时(OP的情况下),categories.emplace_back()不会生成使用Category的移动构造函数的代码,因为它不是noexcept。(当然,在这种情况下没有现有元素需要移动,但这是一个运行时决策;emplace_back必须包括处理一般情况的代码路径,并且该代码路径必须编译。)因此,生成的代码调用Category的复制构造函数,这会导致编译错误。
解决方案是为Category提供一个移动构造函数并将其标记为noexcept(否则它将无济于事)。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的移动构造函数(如果声明了),否则使用复制构造函数(就像隐式声明的移动构造函数一样)。现在,因为构造函数是用户声明的,所以还必须考虑赋值运算符。
问题中第1、3和4条子弹的解释现在应该很清楚了。第2个子弹(使data只是一个单独的unique_ptr<int>)更有趣:
  • unique_ptr有一个删除的复制构造函数;这导致Category的隐式声明复制构造函数也被定义为删除。
  • Category的移动构造函数仍然如上所述声明(在OP的情况下不是noexcept)。
  • 这意味着为emplace_back生成的代码不能使用Category的复制构造函数,因此它必须使用移动构造函数,即使它可能抛出异常(请参见上面的第一节)。代码可以编译,但它不再提供强异常安全保证。

注意:自C++14之后,vector的移动构造函数才被指定为noexcept,这是因为N4258被采纳到工作草案中。然而,在实践中,自C++0x以来,libstdc++和libc++都为vector提供了noexcept移动构造函数;实现允许加强异常规范与标准规范相比,所以这是可以的。
实际上,对于C++14及以下版本,libc++使用noexcept(is_nothrow_move_constructible<allocator_type>::value),但自C++11以来,分配器要求具有nothrow移动和复制构造函数([17.6.3.5]中的表28),因此对于符合标准的分配器来说,这是多余的。
注意(更新):关于强异常安全性保证的讨论不适用于附带MSVC的标准库实现,版本在2017年之前:直到Visual Studio 2015 Update 3为止,它始终尝试移动,而不考虑noexcept规范。
根据Stephan T. Lavavej的这篇博客文章,MSVC 2017中的实现已经进行了彻底的改进,现在行为与上述描述相同。

除非另有说明,标准参考文献为工作草案N4567。


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