Why does QString and vector <unique_ptr <int>> look incompatible here?
I am trying to compile some code that comes down to the following:
#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 leads to the following error from g ++ and similar to 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 the
namemember fromCategory, it compiles fine. - If I create
dataonly oneunique_ptr<int>instead of a pointer vector, it compiles fine. - If I create a single
Categoryinmain()instead of creating a vector and doingemplace_back(), it compiles fine. - If I replace
QStringwithstd::string, it compiles fine.
What's happening? What makes this code poorly formed? Is this the result of errors in g ++ and clang ++?
The key issue here is that std::vector trying to offer an exception safety guarantee for as many operations as possible, but in order to do this, it needs support on the type of element. For push_back , emplace_back and friends, the main problem is what happens if redistribution is required, since existing elements need to be copied / moved to a new repository.
The corresponding standard wording is given in [23.3.6.5p1]:
Notes: causes redistribution if the new size is larger than the old. If redistribution does not occur, all iterators and references before the entry point remains valid. If an exception is selected other than the copy constructor, moving the constructor, assigning an operator or move assignment operator to
Tor anyInputIteratoroperation has no effects. If the exception is the insertion of one element at the end andTisCopyInsertableoris_nothrow_move_constructible<T>::valuetrue, there is no effect. Otherwise, if the exception is notCopyInsertablemove constructor, notCopyInsertableT, the effects are undefined.
(The original wording in C ++ 11 was refined by LWG 2252. )
Note that is_nothrow_move_constructible<T>::value == true does not necessarily mean that T has a move constructor noexcept ; a noexcept copy constructor accepting const T& will also do.
In practice, this means that conceptually, the vector implementation usually tries to generate code for one of the following solutions for copying / moving existing elements to the new storage in descending order of preference ( T is the type of element, and we are interested in class types here):
- If
Thas a useful (existing, non-deleted, non-ambiguous, accessible, etc.)noexceptmovenoexcept, use it; exceptions cannot be thrown when building items in the new repository, so there is no need to return to the previous state. - Otherwise, if
Thas a convenient copy constructor,noexceptor not, which acceptsconst T&, use this; even if copying throws an exception, we can return to the previous state, since the originals still exist, unmodified. - Otherwise, if
Thas a useful move constructor that can throw exceptions, use this; however, a strong guarantee of the safety of exceptions can no longer be offered. - Otherwise, the code does not compile.
The above can be achieved with std::move_if_noexcept or something similar.
Let's see what Category offers in terms of constructors. None is declared explicitly, so the default constructor, copy constructor, and move constructor are implicitly declared.
The copy constructor uses the appropriate member copy constructors:
dataisstd::vector, and thevectorcopy constructor cannot benoexcept(usually you need to allocate new memory), so theCategorycopy constructor cannot benoexceptno matter what theQStringis.- The constructor definition
std::vector<std::unique_ptr<int>>copy calls thestd::unique_ptr<int>copy constructor, which is explicitly deleted, but this only affects the definition, which is created only when necessary. Declaration is needed to resolve overloads, soCategoryhas an implicitly declared copy constructor that will cause a compilation error when called.
Move constructor:
std::vectorhas anoexceptmovenoexcept(see note below), sodatanot a problem.- Older versions of
QString(prior to Qt 5.2):- The move constructor is not explicitly declared (see Praetorian comment above ), therefore, since there is an explicitly declared copy constructor, the move constructor will not be declared implicitly at all.
- Defining an implicitly declared
Categoryconstructor will use theQStringcopy constructor, which takesconst QString&, which can be bound to rvalues (constructors for subobjects are selected using overload resolution). - In these older versions, the
QStringcopy constructor is not specified asnoexcept, so theCategorymove constructor cannot benoexcept.
- Since Qt 5.2,
QStringhas an explicitly declared move constructor that will be used by theCategorymove constructor. However, before Qt 5.5, theQStringmove constructor was notnoexcept, so theCategorymove constructor could not benoexcept. - Since Qt 5.5, the
QStringmove constructor is listed asnoexcept, so theCategorymove constructor is alsonoexcept.
Note that Category has a move constructor in all cases, but it may not be moved by name , and it may not be noexcept .
Given all of the above, we can see that categories.emplace_back() will not generate code that uses the Category move constructor when using Qt 4 (OP case), because it is not noexcept . (Of course, in this case there are no existing elements, but the solution for executing emplace_back time must include the path to the code that handles the general case, and this code must compile.) Thus, the generated code calls the Category copy constructor, which causes a compilation error .
The solution is to create a move constructor for the Category and mark it noexcept (otherwise it will not help). QString uses copy-on-write anyway, so it is unlikely to throw when 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 start the QString move constructor if declared, and use the copy constructor otherwise (just like the implicitly declared move constructor). Now that constructors are declared by the user, you must also consider assignment operators.
Explanations for bullets 1, 3 and 4 in the question should now be pretty clear. Bullet 2 (make data just one unique_ptr<int> ) is more interesting:
unique_ptrhas a remote copy instance; this causes theCategoryimplicitly declared copy constructor to also be defined as remote as well.CategoryThe move constructor is still declared above (notnoexceptin the case of OP).- This means that the code generated for
emplace_backcannot use theCategorycopy constructor, so it must use the move constructor even if it can be selected (see the first section above). The code compiles, but it no longer offers a reliable security guarantee.
Note: vector The move constructor has only recently been specified as noexcept in the standard after C ++ 14 as a result of the adoption of N4258 in a working draft. In practice, however, both libstdC ++ and libC ++ have provided the noexcept move noexcept for vector since C ++ 0x; implementation allows you to strengthen the specification of the exception compared to the standard specification, so OK.
libC ++ actually uses noexcept(is_nothrow_move_constructible<allocator_type>::value) for C ++ 14 and below, but distributors must be intransitive and copied constructive with C ++ 11 (table 28 in [17.6.3.5]), so that redundancy for Standard- Suitable valves.
Note (updated): The discussion of the strong exception security guarantee does not apply to the standard library implementation shipped with MSVC up to version 2017: until Visual Studio 2015 Update 3 is updated, it always tries to move regardless of the noexcept specification.
According to this blog post from Stephan T. Lavavej, the implementation in MSVC 2017 has been redesigned and now behaves correctly as described above.
Standard references apply to Draft N4567, unless otherwise specified.