为什么 QString 和 vector<unique_ptr<int>>在这里显得不兼容? [英] Why do QString and vector<unique_ptr<int>> appear incompatible here?

查看:36
本文介绍了为什么 QString 和 vector<unique_ptr<int>>在这里显得不兼容?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

#include #include <向量>#include 班级分类{std::vector>数据;QString 名称;};int main(){std::vector<类别>类别;category.emplace_back();};

按原样编译,它会导致 g++ 和 clang++ 类似的以下错误:

在/opt/gcc-4.8/include/c++/4.8.2/memory:64:0 包含的文件中,来自 test.cpp:1:/opt/gcc-4.8/include/c++/4.8.2/bits/stl_construct.h:在 'void std::_Construct(_T1*, _Args&& ...) 的实例化中 [with _T1 = std::unique_ptr<国际>;_Args = {const std::unique_ptr>&}]’:/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:75:53: 来自'static _ForwardIterator std::__uninitialized_copy<_TrivialValueTypes>::__uninit_copy(_InputIterator, _InputIterator, _Forward_InputIterator) [= __gnu_cxx::__normal_iterator*, std::vector>>;_ForwardIterator = std::unique_ptr*;bool _TrivialValueTypes = false]’/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:117:41: 来自'_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = __gnu_cxx::__normal_iterator<std::unique_ptr*, std::vector>>;_ForwardIterator = std::unique_ptr*]’/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:258:63: 来自'_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*;_Tp = std::unique_ptr]’/opt/gcc-4.8/include/c++/4.8.2/bits/stl_vector.h:316:32:来自'std::vector<_Tp,_Alloc>::vector(const std::vector<_Tp,_Alloc>);&) [使用 _Tp = std::unique_ptr;_Alloc = std::allocator>]’test.cpp:5:7: [跳过 2 个实例化上下文,使用 -ftemplate-backtrace-limit=0 禁用]/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:117:41: 来自‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = Category*;_ForwardIterator = 类别*]’/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:258:63: 来自'_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&)[与 _InputIterator = Category*;_ForwardIterator = 类别*;_Tp = 类别]’/opt/gcc-4.8/include/c++/4.8.2/bits/stl_uninitialized.h:281:69: 来自'_ForwardIterator std::__uninitialized_move_if_noexcept_a(_InputIterator, _InputIterator, _ForwardIterator, _Allocator&InputIterator) 的要求; = [with Category*_ForwardIterator = 类别*;_Allocator = std::allocator<类别>]’/opt/gcc-4.8/include/c++/4.8.2/bits/vector.tcc:415:43: 来自 'void std::vector<_Tp, _Alloc>::_M_emplace_back_aux(_Args&& ...)[与 _Args = {};_Tp = 类别;_Alloc = std::allocator<类别>]’/opt/gcc-4.8/include/c++/4.8.2/bits/vector.tcc:101:54: 来自 'void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...)[与 _Args = {};_Tp = 类别;_Alloc = std::allocator<类别>]’test.cpp:14:29: 从这里需要/opt/gcc-4.8/include/c++/4.8.2/bits/stl_construct.h:75:7: 错误:使用已删除的函数 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<;_Tp, _Dp>&) [其中 _Tp = int;_Dp = std::default_delete]’{ ::new(static_cast(__p)) _T1(std::forward<_Args>(__args)...);}^在/opt/gcc-4.8/include/c++/4.8.2/memory:81:0 包含的文件中,来自 test.cpp:1:/opt/gcc-4.8/include/c++/4.8.2/bits/unique_ptr.h:273:7: 错误:此处声明unique_ptr(const unique_ptr&) = 删除;^

  • 如果我从 Category 中删除 name 成员,它编译正常.
  • 如果我让 data 只是一个 unique_ptr 而不是一个指针向量,它编译得很好.
  • 如果我在 main() 中创建一个 Category 而不是创建一个向量并执行 emplace_back(),它编译得很好.
  • 如果我用 std::string 替换 QString,它编译得很好.

这是怎么回事?是什么让这段代码格式错误?是 g++ & 中的错误的结果吗?叮当++?

解决方案

这里的关键问题是 std::vector 试图提供 为尽可能多的操作提供强大的异常安全保证,但是,为了做到这一点,它需要元素类型的支持.对于push_backemplace_back 和朋友来说,主要的问题是如果需要重新分配会发生什么,因为需要将现有元素复制/移动到新的存储中.>

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

<块引用>

备注: 如果新容量大于旧容量,则导致重新分配.如果没有发生重新分配,所有的迭代器和引用在插入点保持有效之前.如果抛出异常除了通过复制构造函数、移动构造函数、赋值运算符,或移动 T 或任何 InputIterator 的赋值运算符操作没有任何影响.如果同时抛出异常在末尾插入一个元素并且 TCopyInsertableis_nothrow_move_constructible::valuetrue,没有效果.否则,如果移动构造函数抛出异常非CopyInsertable T 的效果未指定.

(C++11 中的原始措辞已通过 LWG 2252.)

注意 is_nothrow_move_constructible::value == true 并不一定意味着 T 有一个 noexcept 移动构造函数;一个采用 const T&noexcept 拷贝构造函数也可以.

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

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

以上可以通过使用std::move_if_noexcept 或类似的东西.

<小时>

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

拷贝构造函数使用成员各自的拷贝构造函数:

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

移动构造函数:

  • std::vector 有一个 noexcept 移动构造函数(见下面的注释),所以 data 不是问题.
  • QString 的旧版本(Qt 5.2 之前):
    • 未明确声明移动构造函数(参见 Praetorian's comment above),所以,因为有一个显式声明的复制构造函数,移动构造函数将不会被隐式声明.
    • 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.

<小时>

鉴于以上所有内容,我们可以看到 categories.emplace_back() 在使用 Qt 4 时不会生成使用 Category 的移动构造函数的代码(OP 的情况),因为它不是 noexcept.(当然,在这种情况下没有要移动的现有元素,但这是一个运行时决定;emplace_back 必须包含处理一般情况的代码路径,并且该代码路径必须编译.)所以,生成的代码调用了Category的拷贝构造函数,导致编译错误.

一个解决方案是为Category 提供一个移动构造函数并将其标记为noexcept(否则它不会有帮助).QString 无论如何都使用 copy-on-write,所以它不太可能在复制时抛出.

这样的事情应该可以工作:

class 分类{std::vector>数据;QString 名称;民众:类别()=默认;Category(const Category&) = default;类别(Category&& c) noexcept : data(std::move(c.data)), name(std::move(c.name)) { }//赋值运算符};

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

问题中第 1、3 和 4 项的解释现在应该很清楚了.Bullet 2(使 data 只是一个 unique_ptr)更有趣:

  • unique_ptr 有一个删除的复制构造函数;这会导致 Category 隐式声明的复制构造函数也被定义为已删除.
  • Category 的移动构造函数仍然如上声明(在 OP 的情况下不是 noexcept).
  • 这意味着为 emplace_back 生成的代码不能使用 Category 的复制构造函数,所以它必须使用移动构造函数,即使它可以抛出(参见以上第一节).代码可以编译,但不再提供强大的异常安全保证.
<小时>

注意:vector 的移动构造函数最近才在标准中指定为 noexcept,在 C++14 之后,由于采用了 N4258 进入工作草案.然而,在实践中,libstdc++ 和 libc++ 从 C++0x 时代就为 vector 提供了一个 noexcept 移动构造函数;与标准的规范相比,允许实现加强异常规范,所以没关系.

libc++ 实际上使用 noexcept(is_nothrow_move_constructible<allocator_type>::value) 用于 C++14 及以下,但自 C++11(表 28在 [17.6.3.5] 中),所以这对于符合标准的分配器来说是多余的.

<小时>

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

根据 这篇由 Stephan T. Lavavej 撰写的博文,MSVC 2017 中的实现已经过大修,现在可以正常运行,如上所述.

<小时>

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

I'm trying to compile some code, which reduces to this:

#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();
};

Compiled as is, it results in the following error from g++ and similar for 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;
       ^

  • If I remove name member from Category, it compiles fine.
  • If I make data just a single unique_ptr<int> instead of a vector of pointers, it compiles fine.
  • If I create a single Category in main() instead of a creating a vector and doing emplace_back(), it compiles fine.
  • If I replace QString with std::string, it compiles fine.

What's going on? What makes this code ill-formed? Is it result of bugs in g++ & clang++?

解决方案

The key issue here is that std::vector tries to offer the strong exception safety guarantee for as many operations as possible, but, in order to do that, it needs support from the element type. For push_back, emplace_back and friends, the main problem is what happens if a reallocation is necessary, as the existing elements need to be copied / moved to the new storage.

The relevant standard wording is in [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.

(The original wording in C++11 has been clarified by the resolution of LWG 2252.)

Note that is_nothrow_move_constructible<T>::value == true doesn't necessarily mean that T has a noexcept move constructor; a noexcept copy constructor taking const T& will do as well.

What this means in practice is that, conceptually, a vector implementation typically tries to generate code for one of the following solutions for copying / moving existing elements to the new storage, in decreasing order of preference (T is the element type, and we're interested in class types here):

  • If T has a usable (present, not deleted, not ambiguous, accessible, etc.) noexcept move constructor, use it; exceptions cannot be thrown while constructing the elements in the new storage, so there's no need to revert to the previous state.
  • Otherwise, if T has a usable copy constructor, noexcept or not, that takes a const T&, use that; even if copying throws an exception, we can revert to the previous state, as the originals are still there, unmodified.
  • Otherwise, if T has a usable move constructor that may throw exceptions, use that; however, the strong exception safety guarantee cannot be offered anymore.
  • Otherwise, the code doesn't compile.

The above can be achieved by using std::move_if_noexcept or something similar.


Let's see what Category offers in terms of constructors. None is declared explicitly, so a default constructor, a copy constructor and a move constructor are implicitly declared.

The copy constructor uses the respective copy constructors of the members:

  • data is a std::vector, and vector's copy constructor cannot be noexcept (it generally needs to allocate new memory), so Category's copy constructor cannot be noexcept regardless of what QString has.
  • The definition of std::vector<std::unique_ptr<int>>'s copy constructor calls std::unique_ptr<int>'s copy constructor, which is explicitly deleted, but this only affects the definition, which is only instantiated if needed. Overload resolution only needs the declarations, so Category has an implicitly declared copy constructor that will cause a compile error if called.

The move constructor:

  • std::vector has a noexcept move constructor (see the note below), so data is not a problem.
  • Old versions of QString (before Qt 5.2):
    • A move constructor is not explicitly declared (see Praetorian's comment above), so, because there is an explicitly declared copy constructor, a move constructor will not be implicitly declared at all.
    • The definition of the implicitly declared move constructor of Category will use QString's copy constructor that takes a const QString&, which can bind to rvalues (the constructors for subobjects are chosen using overload resolution).
    • In these old versions, QString's copy constructor is not specified as noexcept, so Category's move constructor can't be noexcept either.
  • Since Qt 5.2, QString has an explicitly declared move constructor, which will be used by Category's move constructor. However, before Qt 5.5, QString's move constructor was not noexcept, so Category's move constructor can't be noexcept either.
  • Since Qt 5.5, QString's move constructor is specified as noexcept, so Category's move constructor is noexcept as well.

Note that Category does have a move constructor in all cases, but it may not move name, and it may not be noexcept.


Given all of the above, we can see that categories.emplace_back() won't generate code that uses Category's move constructor when Qt 4 is used (OP's case), because it's not noexcept. (Of course, there are no existing elements to move in this case, but that's a runtime decision; emplace_back has to include a code path that handles the general case, and that code path has to compile.) So, the generated code calls Category's copy constructor, which causes the compile error.

A solution is to provide a move constructor for Category and mark it noexcept (otherwise it won't help). QString uses copy-on-write anyway, so it's unlikely to throw while copying.

Something like this should work:

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
};

This will pick up QString's move constructor if declared, and use the copy constructor otherwise (just like the implicitly declared move constructor would). Now that the constructors are user-declared, the assignment operators have to be taken into account as well.

The explanations for bullets 1, 3 and 4 in the question should now be pretty clear. Bullet 2 (make data just a single unique_ptr<int>) is more interesting:

  • unique_ptr has a deleted copy constructor; this causes Category's implicitly declared copy constructor to be defined as deleted as well.
  • Category's move constructor is still declared as above (not noexcept in the OP's case).
  • This means that the code generated for emplace_back cannot use Category's copy constructor, so it has to use the move constructor, even though it can throw (see the first section above). The code compiles, but it no longer offers the strong exception safety guarantee.

Note: vector's move constructor has only recently been specified as noexcept in the Standard, after C++14, as a result of the adoption of N4258 into the working draft. In practice, however, both libstdc++ and libc++ have provided a noexcept move constructor for vector since the times of C++0x; an implementation is allowed to strengthen an exception specification compared to the Standard's specification, so that's OK.

libc++ actually uses noexcept(is_nothrow_move_constructible<allocator_type>::value) for C++14 and below, but allocators are required to be nothrow move and copy constructible since C++11 (table 28 in [17.6.3.5]), so that's redundant for Standard-conforming allocators.


Note (updated): The discussion about the strong exception safety guarantee doesn't apply to the standard library implementation that comes with MSVC before version 2017: up to and including Visual Studio 2015 Update 3, it always tries to move, regardless of the noexcept specification.

According to this blog post by Stephan T. Lavavej, the implementation in MSVC 2017 has been overhauled and now behaves correctly as described above.


Standard references are to working draft N4567 unless otherwise noted.

这篇关于为什么 QString 和 vector&lt;unique_ptr&lt;int&gt;&gt;在这里显得不兼容?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆