Почему 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 Эффектов нет. В противном случае, если исключение выдается конструктором перемещения CopyInsertableT эффекты не уточняются.

(Первоначальная формулировка в 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.5 QString Ход конструктора не был 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, если не указано иное.

Другие вопросы по тегам