2015-12-07 34 views
5

我試圖編譯一些代碼,這減少了這一點:爲什麼QString和vector <unique_ptr <int>>在這裏顯示爲不兼容?

#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(),它編譯得很好。
  • 如果我用std::string代替QString,它編譯得很好。

發生了什麼事?是什麼讓這個代碼不合格?是g ++ & clang ++的bug的結果嗎?

+1

可能是因爲'QString'定義了一個拷貝構造函數,並且沒有移動構造函數,這意味着它的移動構造函數被隱式定義爲刪除。然後'emplace_back'調用將嘗試複製'unique_ptr',導致上面的錯誤。 – Praetorian

+0

根據[this](http://doc.qt.io/qt-5/qstring.html#QString-6),'QString'的移動構造函數是在Qt5.2中引入的,你使用的是舊版本? – Praetorian

+0

@Praetorian我在這裏使用Qt4。 – Ruslan

回答

4

這裏的關鍵問題是std::vector試圖爲儘可能多的操作提供strong exception safety guarantee,但爲了做到這一點,它需要來自元素類型的支持。對於push_back,emplace_back和朋友,主要問題是如果需要重新分配會發生什麼情況,因爲現有元素需要複製/移動到新存儲中。

相關的標準寫法是[23.3.6.5p1]:

注:導致重新分配,如果新的尺寸比舊款更大的容量。如果沒有重新分配,插入點之前的所有迭代器和引用 保持有效。如果除複製構造函數以外的其他移動構造函數,移動構造函數,賦值 運算符或移動賦值運算符T或任何InputIterator 操作之外引發異常 沒有影響。如果發生異常,而 在末尾插入單個元素,而TCopyInsertableis_nothrow_move_constructible<T>::valuetrue,則不存在 的影響。否則,如果非移動構造函數 引發異常,則效果不明確。

(在C++ 11的最初的措辭已經通過LWG 2252分辨率澄清。)

注意is_nothrow_move_constructible<T>::value == true並不一定意味着T具有noexcept移動構造函數;一個noexcept複製構造函數將採取const T&也會。

這意味着在實踐中是,概念性,一個vector實現通常嘗試爲要複製的以下解決方案之一生成的代碼/移動現有元素到新的存儲,以遞減的優先級順序(T是元素類型,我們感興趣的是這裏的類類型):

  • 如果T有一個可用的(目前,沒有被刪除,也不含糊,訪問等)noexcept移動構造函數,使用它;在構建新存儲中的元素時不會拋出異常,因此不需要恢復到之前的狀態。
  • 否則,如果T有一個可用的拷貝構造函數,noexcept或者沒有,需要const T&,使用它;即使複製拋出異常,我們也可以恢復到之前的狀態,因爲原稿仍然存在,未經修改。
  • 否則,如果T有一個可用的移動構造函數可能會拋出異常,請使用它;然而,強大的例外安全保證不能再提供。
  • 否則,代碼不會編譯。

以上可以通過使用std::move_if_noexcept或類似的東西來實現。


讓我們來看看Category提供的構造函數。沒有顯式聲明,所以默認的構造函數,複製構造函數和移動構造函數都是隱式聲明的。

複製構造使用成員的相應副本構造:

  • datastd::vector,和vector的拷貝構造不能noexcept(它一般需要分配新的內存),所以Category的不管QString具有什麼,複製構造函數不能是noexcept
  • std::vector<std::unique_ptr<int>>的拷貝構造函數的定義調用std::unique_ptr<int>的拷貝構造函數,它被顯式刪除,但是這隻影響定義,如果需要的話只會被實例化。重載解析只需要聲明,所以Category有一個隱式聲明的複製構造函數,如果調用它將導致編譯錯誤。

此舉構造:

  • std::vectornoexcept移動構造函數(見下面的註釋),所以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 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移動構造函數;與標準規範相比,允許實現強化異常規範,所以沒關係。

對於C++ 14及更低版本,libC++實際上使用noexcept(is_nothrow_move_constructible<allocator_type>::value),但自從C++ 11([17.6.3.5])中的表28以來,分配器被要求不能移動和複製可構造,因此對於標準符合分配器。


注(更新):有關強異常安全保證的討論並不適用於隨MSVC之前2017年版的標準庫的實現:直至幷包括Visual Studio的2015年更新3,它總是試圖無論noexcept規範如何移動。

根據Stephan T. Lavavej的this blog post,MSVC 2017中的實現已被徹底修改,現在的行爲如上所述正確。


除非另有說明,否則標準參考是工作草案N4567。

相關問題