2015-12-07 38 views
5

sto cercando di compilare un codice, che si riduce a questo:Perché QString e il vettore <unique_ptr <int>> non sono compatibili qui?

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

compilati come è, il risultato e 'il seguente errore da g ++ e simile per 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; 
    ^
  • Se Rimuovere name membro da Category, compila bene.
  • Se faccio solo un singolo unique_ptr<int> invece di un vettore di puntatori, compila bene.
  • Se creo un singolo Category in main() invece di creare un vettore e lo faccio emplace_back(), esso viene compilato correttamente.
  • Se sostituisco QString con std::string, compila bene.

Cosa sta succedendo? Cosa rende questo codice mal formato? E 'il risultato di bug in g ++ & clang ++?

+1

Potrebbe essere dovuto al fatto che 'QString' definisce un costruttore di copie e nessun costruttore di spostamento, il che significa che il suo costruttore di spostamento è implicitamente definito come eliminato. Quindi la chiamata 'emplace_back' tenterà di copiare' unique_ptr' risultante negli errori di cui sopra. – Praetorian

+0

Secondo [this] (http://doc.qt.io/qt-5/qstring.html#QString-6), il costruttore di spostamenti 'QString' è stato introdotto in Qt5.2, stai usando una versione precedente ? – Praetorian

+0

@Praetorian Sto usando Qt4 qui. – Ruslan

risposta

4

Il problema chiave qui è che std::vector tenta di offrire il strong exception safety guarantee per quante più operazioni possibile, ma, per fare ciò, ha bisogno del supporto del tipo di elemento. Per push_back, emplace_back e gli amici, il problema principale è che cosa succede se è necessaria una riallocazione, poiché gli elementi esistenti devono essere copiati/spostati nella nuova memoria.

La formulazione standard di riferimento è in [23.3.6.5p1]:

Osservazioni: Provoca riallocazione se la nuova dimensione è superiore alla vecchia capienza. Se non si verifica alcuna riallocazione, tutti gli iteratori e i riferimenti prima del punto di inserimento rimangono validi. Se viene generata un'eccezione oltre che dal costruttore di copie, dal costruttore di movimento, dall'operatore di assegnazione o dall'operatore di spostamento T o da qualsiasi operazione InputIterator , non ci sono effetti. Se viene generata un'eccezione mentre inserimento di un singolo elemento alla fine e T è CopyInsertable o is_nothrow_move_constructible<T>::value è true, non ci sono effetti . Altrimenti, se viene generata un'eccezione dal costruttore di spostamenti di un numero non CopyInsertableT, gli effetti non sono specificati.

(La formulazione originale in C++ 11 è stata chiarita dalla risoluzione del LWG 2252.)

noti che is_nothrow_move_constructible<T>::value == true non significa necessariamente che T ha un costruttore noexcept mossa; un costruttore di copie noexcept che prende const T& farà altrettanto.

Ciò significa in pratica che, concettualmente, un'implementazione vector cerca tipicamente per generare il codice per una delle seguenti soluzioni per la copia/spostamento elementi esistenti al nuovo stoccaggio, in ordine decrescente di preferenza (T è il tipo di elemento, e siamo interessati a tipi di classe qui):

  • Se T ha una utilizzabile (presente, non eliminati, non ambiguo, accessibili, ecc) noexcept mossa costruttore, usarlo; le eccezioni non possono essere lanciate mentre si costruiscono gli elementi nel nuovo archivio, quindi non è necessario tornare allo stato precedente.
  • Altrimenti, se T dispone di un costruttore di copia utilizzabile, noexcept o meno, che utilizza uno const T&; anche se la copia genera un'eccezione, possiamo tornare allo stato precedente, poiché gli originali sono ancora lì, non modificati.
  • In caso contrario, se T dispone di un costruttore di spostamento utilizzabile che può generare eccezioni, utilizzare tale; tuttavia, la forte garanzia di sicurezza eccezionale non può più essere offerta.
  • In caso contrario, il codice non viene compilato.

Quanto sopra può essere ottenuto utilizzando std::move_if_noexcept o qualcosa di simile.


Vediamo cosa Category offre in termini di costruttori. Nessuno è dichiarato esplicitamente, quindi un costruttore predefinito, un costruttore di copie e un costruttore di movimento sono dichiarati implicitamente.

Il costruttore di copia utilizza rispettivi costruttori di copia dei membri:

  • data è un std::vector e vector 's costruttore di copia non può essere noexcept (generalmente necessario allocare nuova memoria), quindi Category' s copy constructor non può essere noexcept indipendentemente da ciò che ha QString.
  • La definizione di costruttore di copia std::vector<std::unique_ptr<int>> chiama il costruttore di copie std::unique_ptr<int>, che viene eliminato esplicitamente, ma questo influenza solo la definizione, che viene solo istanziata se necessario. La risoluzione di sovraccarico richiede solo le dichiarazioni, quindi Category ha un costruttore di copie implicitamente dichiarato che causerà un errore di compilazione se chiamato.

La mossa costruttore:

  • std::vector ha un movimento costruttore noexcept (vedi nota sotto), quindi data non è un problema.
  • Le vecchie versioni di QString (prima di Qt 5.2):
    • Un costruttore mossa non è esplicitamente dichiarata (vedi Praetorian's comment above), quindi, perché non v'è un costruttore di copia esplicitamente dichiarato, un costruttore mossa non verranno implicitamente dichiarate del tutto.
    • La definizione del costruttore di spostamento implicitamente dichiarata di Category utilizzerà il costruttore di copie QString che accetta un valore const QString&, che può essere associato a valori rvalue (i costruttori per i sottoggetti sono scelti utilizzando la risoluzione di sovraccarico).
    • In queste versioni precedenti, il costruttore di copia QString non è specificato come noexcept, quindi il costruttore di spostamento Category non può essere noexcept.
  • Dal Qt 5.2, QString ha un costruttore mossa esplicitamente dichiarato, che verrà utilizzato da Category 's costruttore mossa. Tuttavia, prima di Qt 5.5, il costruttore di spostamento QString non era noexcept, quindi il costruttore di spostamento Category non può essere noexcept.
  • Poiché il costruttore di movimento Qt 5.5, QString è specificato come noexcept, il costruttore di spostamento di Category è noexcept.

Nota che Category ha un costruttore di movimento in tutti i casi, ma non può muoversi name, e non può essere noexcept.


Dato tutto quanto sopra, si può vedere che categories.emplace_back() non genererà codice che utilizza Category s' mossa costruttore quando viene utilizzato Qt 4 (caso PO), perché non è noexcept. (Naturalmente, non ci sono elementi esistenti da spostare in questo caso, ma questa è una decisione runtime: emplace_back deve includere un percorso di codice che gestisce il caso generale e che il percorso del codice deve compilare.) Quindi il codice generato chiama Category Il costruttore di copie, che causa l'errore di compilazione.

Una soluzione è fornire un costruttore di spostamenti per Category e contrassegnarlo come noexcept (altrimenti non sarà di aiuto). QString utilizza comunque copy-on-write, quindi è improbabile che venga lanciata durante la copia.

Qualcosa del genere dovrebbe funzionare:

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

Questo pick up costruttore mossa QString s' se dichiarato, e utilizzare il costruttore di copia altrimenti (proprio come il costruttore mossa implicitamente dichiarato sarebbe). Ora che i costruttori sono dichiarati dall'utente, anche gli operatori di assegnazione devono essere presi in considerazione.

Le spiegazioni per i punti 1, 3 e 4 nella domanda ora dovrebbero essere abbastanza chiare. Punto 2 (fare data solo un singolo unique_ptr<int>) è più interessante:

  • unique_ptr ha un costruttore di copia eliminata; questo fa sì che il costruttore di copie implicitamente dichiarato di Category sia definito come cancellato.
  • Il costruttore di movimento Category è ancora dichiarato come sopra (non noexcept nel caso dell'OP).
  • Ciò significa che il codice generato per emplace_back non può utilizzare il costruttore di copia Category, quindi deve utilizzare il costruttore di movimento, anche se può lanciare (vedere la prima sezione sopra). Il codice viene compilato, ma non offre più una garanzia di sicurezza eccezionale.

Nota: s' vector costruttore mossa è stata solo recentemente specificato come noexcept nello Standard, dopo C++ 14, a seguito dell'adozione del N4258 nella bozza di lavoro. In pratica, tuttavia, sia libstdC++ che libC++ hanno fornito un costruttore di spostamenti noexcept per vector dai tempi di C++ 0x; Un'implementazione può rafforzare una specifica di eccezione rispetto alle specifiche dello standard, quindi va bene.

libC++ utilizza effettivamente noexcept(is_nothrow_move_constructible<allocator_type>::value) per C++ 14 e versioni precedenti, ma gli allocatori devono essere nothrow move e copy constructible dal C++ 11 (tabella 28 in [17.6.3.5]), quindi è ridondante per conformità standard ripartitori.


Nota (aggiornato): La discussione circa la forte garanzia di sicurezza eccezione non si applica alla realizzazione libreria standard che viene fornito con MSVC prima della versione 2017: fino al Visual Studio 2015 Update 3, cerca sempre per spostarsi, indipendentemente dalle specifiche no-cut.

Secondo lo this blog post di Stephan T. Lavavej, l'implementazione in MSVC 2017 è stata revisionata e ora si comporta correttamente come descritto sopra.


I riferimenti standard si riferiscono alla bozza di lavoro N4567, se non diversamente specificato.