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

Why do QString and vector<unique_ptr<int>> appear incompatible here?

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

#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;
       ^

这是怎么回事?是什么导致这段代码格式错误?它是 g++ 和 clang++ 中错误的结果吗?

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

相关标准写法在[23.3.6.5p1]:

Remarks: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid. If an exception is thrown other than by the copy constructor, move constructor, assignment operator, or move assignment operator of T or by any InputIterator operation there are no effects. If an exception is thrown while inserting a single element at the end and T is CopyInsertable or is_nothrow_move_constructible<T>::value is true, there are no effects. Otherwise, if an exception is thrown by the move constructor of a non-CopyInsertable T, the effects are unspecified.

(C++11中的原写法已经被LWG 2252的决议澄清了。)

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

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

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

以上可以通过使用std::move_if_noexcept或类似的东西来实现。


让我们看看 Category 在构造函数方面提供了什么。 None 被显式声明,因此默认构造函数、复制构造函数和移动构造函数被隐式声明。

复制构造函数使用成员各自的复制构造函数:

  • data是一个std::vector,而vector的拷贝构造函数不能是noexcept(一般需要分配新的内存),所以Category的复制构造函数不能是noexcept,不管QString有什么。
  • std::vector<std::unique_ptr<int>>的拷贝构造函数的定义调用了std::unique_ptr<int>的拷贝构造函数,显式删除了,但这只影响定义,只在需要时实例化。重载决议只需要声明,所以 Category 有一个隐式声明的复制构造函数,如果调用它会导致编译错误。

移动构造函数:

  • std::vector 有一个 noexcept 移动构造函数(见下面的注释),所以 data 不是问题。
  • QString 的旧版本(Qt 5.2 之前):
    • 移动构造函数没有显式声明(参见),因此,因为有一个显式声明的复制构造函数,移动构造函数根本不会被隐式声明.
    • Category 的隐式声明移动构造函数的定义将使用 QString 的复制构造函数,它采用 const 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的复制构造函数,所以它必须使用移动构造函数,即使它可以抛出(见上面的第一部分) .代码可以编译,但它不再提供强大的异常安全保证。

注意:vector 的移动构造函数最近才在标准中指定为 noexcept,在 C++14 之后,作为采用 N4258 的结果工作草案。然而,实际上,自 C++0x 时代以来,libstdc++ 和 libc++ 都为 vector 提供了 noexcept 移动构造函数;与标准规范相比,允许实现加强异常规范,所以没关系。

libc++ 实际上对 C++14 及以下版本使用 noexcept(is_nothrow_move_constructible<allocator_type>::value),但自 C++11 以来,分配器必须是不可抛出移动和复制构造的([17.6.3.5] 中的 table 28 ),所以这对于符合标准的分配器来说是多余的。


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

根据 Stephan T.Lavavej this blog post 的说法,MSVC 2017 中的实现已经过全面检查,现在可以正常运行,如上所述。


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