Почему 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 ++ и аналогичной для 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;
^
- Если я удалю
name
член отCategory
, это хорошо компилируется. - Если я сделаю
data
только одинunique_ptr<int>
вместо вектора указателей он компилируется нормально. - Если я создам один
Category
вmain()
вместо создания вектора и делатьemplace_back()
, это хорошо компилируется. - Если я заменю
QString
сstd::string
, это хорошо компилируется.
В чем дело? Что делает этот код плохо сформированным? Это результат ошибок в g ++ и clang++?
1 ответ
Ключевой вопрос здесь заключается в том, что std::vector
пытается предложить надежную гарантию безопасности исключений для максимально возможного числа операций, но для этого ему требуется поддержка со стороны типа элемента. За push_back
, emplace_back
и друзья, основная проблема заключается в том, что происходит, если необходимо перераспределение, так как существующие элементы необходимо скопировать / переместить в новое хранилище.
Соответствующая стандартная формулировка содержится в [23.3.6.5p1]:
Примечания: Вызывает перераспределение, если новый размер больше, чем старая емкость. Если перераспределение не происходит, все итераторы и ссылки до точки вставки остаются действительными. Если исключение выдается кроме конструктора копирования, конструктора перемещения, оператора присваивания или оператора присваивания перемещения
T
или любымInputIterator
операции нет никаких эффектов. Если исключение выдается при вставке одного элемента в конце иT
являетсяCopyInsertable
или жеis_nothrow_move_constructible<T>::value
являетсяtrue
Эффектов нет. В противном случае, если исключение выдается конструктором перемещенияCopyInsertable
T
эффекты не уточняются.
(Первоначальная формулировка в 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
предложения с точки зрения конструкторов. Ничто не объявляется явно, поэтому неявно объявляются конструктор по умолчанию, конструктор копирования и конструктор перемещения.
Конструктор копирования использует соответствующие конструкторы копирования членов:
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&
, который может привязываться к значениям r (конструкторы для подобъектов выбираются с использованием разрешения перегрузки). - В этих старых версиях
QString
конструктор копирования не указан какnoexcept
, такCategory
ход конструктора не может бытьnoexcept
или.
- Начиная с Qt 5.2,
QString
имеет явно объявленный конструктор перемещения, который будет использоватьсяCategory
Ходит конструктор. Однако до Qt 5.5QString
Ход конструктора не былnoexcept
, такCategory
ход конструктора не может бытьnoexcept
или. - Начиная с Qt 5.5,
QString
Конструктор ходов указан какnoexcept
, такCategory
ход конструктораnoexcept
также.
Обратите внимание, что Category
есть конструктор перемещения во всех случаях, но он может не двигаться name
и не может быть noexcept
,
Учитывая все вышесказанное, мы можем видеть, что categories.emplace_back()
не будет генерировать код, который использует Category
конструктор перемещения, когда используется Qt 4 (случай OP), потому что это не 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
Конструктор перемещения все еще объявлен как указано выше (неnoexcept
в случае ОП).- Это означает, что код, сгенерированный для
emplace_back
не можете использоватьCategory
Это конструктор копирования, поэтому он должен использовать конструктор перемещения, даже если он может генерировать (см. первый раздел выше). Код компилируется, но больше не дает строгой гарантии безопасности исключений.
Замечания: vector
Конструктор ходов только недавно был указан как noexcept
в Стандарте, после C++14, в результате принятия N4258 в рабочий проект. На практике, однако, и libstdC++, и libC++ обеспечили noexcept
переместить конструктор для vector
со времен C++0x; реализация допускает усиление спецификации исключений по сравнению со спецификацией стандарта, так что все в порядке.
libC++ на самом деле использует noexcept(is_nothrow_move_constructible<allocator_type>::value)
для C++ 14 и ниже, но распределители должны быть на ходу и копировать конструктивно, начиная с C++11 (таблица 28 в [17.6.3.5]), так что это избыточно для распределителей, соответствующих стандартам.
Примечание (обновлено). Обсуждение гарантии безопасности исключений не относится к стандартной реализации библиотеки, которая поставляется с MSVC до версии 2017: вплоть до Обновления 3 для Visual Studio 2015 включительно, он всегда пытается переместиться, независимо от исключений Спецификация.
Согласно этому сообщению в блоге Стефана Т. Лававея, реализация в MSVC 2017 была пересмотрена и теперь ведет себя правильно, как описано выше.
Стандартные ссылки на рабочий проект N4567, если не указано иное.