Я проверил много потоков move constructor / vector / noexcept, но я все еще не уверен, что на самом деле происходит, когда что-то пойдет не так. Я не могу выдать ошибку, когда ожидаю, поэтому либо мой маленький тест неверен, либо мое понимание проблемы неверно.

Я использую вектор объекта BufferTrio, который определяет конструктор перемещения noexcept (false) и удаляет все остальные операторы конструктора / присваивания, чтобы не к чему было отступать:

    BufferTrio(const BufferTrio&) = delete;
    BufferTrio& operator=(const BufferTrio&) = delete;
    BufferTrio& operator=(BufferTrio&& other) = delete;

    BufferTrio(BufferTrio&& other) noexcept(false)
        : vaoID(other.vaoID)
        , vboID(other.vboID)
        , eboID(other.eboID)
    {
        other.vaoID = 0;
        other.vboID = 0;
        other.eboID = 0;
    }

Вещи компилируются и запускаются, но из https://xinhuang.github.io/posts/2013-12-31-when-to-use-noexcept-and-when-to-not.html:

std :: vector будет использовать move, когда ему нужно увеличить (или уменьшить) емкость, если только операция перемещения не исключает.

Или из Оптимизированного C ++: проверенные методы для повышения производительности Курта Гюнтерота :

Если конструктор перемещения и оператор присваивания перемещения не объявлены как noexcept, вместо этого std :: vector использует менее эффективные операции копирования.

Поскольку я удалил их, я понимаю, что здесь что-то должно ломаться. Но все идет хорошо с этим вектором. Поэтому я также создал базовый цикл, который полмиллиона раз push_backs превращает в фиктивный вектор, а затем заменяет этот вектор другим фиктивным вектором с одним элементом. Вот так:

    vector<BufferTrio> thing;

    int n = 500000;
    while (n--)
    {
        thing.push_back(BufferTrio());
    }

    vector<BufferTrio> thing2;
    thing2.push_back(BufferTrio());

    thing.swap(thing2);
    cout << "Sizes are " << thing.size() << " and " << thing2.size() << endl;
    cout << "Capacities are " << thing.capacity() << " and " << thing2.capacity() << endl;

Выход:

Sizes are 1 and 500000
Capacities are 1 and 699913

По-прежнему проблем нет, так что

Должен ли я увидеть, что что-то идет не так, и если да, то как я могу это продемонстрировать?

9
Xenial 22 Окт 2017 в 15:55

3 ответа

Лучший ответ

Перераспределение вектора пытается предложить гарантию исключения, то есть попытку сохранить исходное состояние, если исключение выдается во время операции перераспределения. Есть три сценария:

  1. Тип элемента nothrow_move_constructible: перераспределение может перемещать элементы, которые не вызовут исключение. Это эффективный случай.

  2. Тип элемента: CopyInsertable: если тип не может быть nothrow_move_constructible, этого достаточно для обеспечения строгой гарантии, хотя копии создаются во время перераспределения. Это было старое поведение C ++ 03 по умолчанию и менее эффективный запасной вариант.

  3. Тип элемента не является ни CopyInsertable, ни nothrow_move_constructible. До тех пор, пока он все еще является конструируемым при перемещении, как в вашем примере, перераспределение вектора возможно, но не предоставляет никаких исключительных гарантий (например, вы можете потерять элементы, если сработает конструкция перемещения).

Нормативная формулировка, которая говорит, что это распределено по различным перераспределяющим функциям. Например, [vector.modifiers] / push_back говорит:

Если исключение выдается во время вставка одного элемента в конец и T - CopyInsertable или is_nothrow_move_constructible_v<T> - true, никаких эффектов нет. В противном случае, если исключение выдается конструктором перемещения не - CopyInsertable T, эффекты не определены.

Я не знаю, что имели в виду авторы цитируемых вами постов, хотя могу представить, что они неявно предполагают, что вам нужна гарантия строгого исключения, и поэтому они хотели бы направить вас в дела (1) или ( 2 ) .

10
Kerrek SB 22 Окт 2017 в 13:31

TLDR Пока тип MoveInsertable , все в порядке, а здесь все в порядке.

Типом является MoveInsertable , если, с учетом распределителя контейнера A, экземпляр распределителя назначен переменной m, указатель на T* называется {{X3} } и r-значение типа T следующее выражение корректно сформировано:

allocator_traits<A>::construct(m, p, rv);

Для вашего std::vector<BufferTrio> вы используете по умолчанию std::allocator<BufferTrio>, так что этот вызов construct вызывает

m.construct(p, std::forward<U>(rv))

Где U - это ссылочный тип пересылки (в вашем случае это BufferTrio&&, ссылка на значение)

Все идет нормально,

m.construct будет использовать place-new для создания члена на месте ([allocator.members])

::new((void *)p) U(std::forward<Args>(args)...)

Это ни в коем случае не требует require noexcept. Это только для исключительных гарантийных причин.

[vector.modifiers] утверждает, что для void push_back(T&& x);

Если исключение выдается ходом конструктор не - CopyInsertable T, эффекты не определены.


В заключение, Что касается вашего swap ( Акцент на мой ):

[ Container.requirements.general ]

Выражение a.swap(b) для контейнеров a и b стандартного типа контейнера, отличного от array, должно поменяться значения a и b без вызова каких-либо операций перемещения, копирования или обмена в отдельном контейнере Элементы .

3
AndyG 22 Окт 2017 в 13:21

В вашем примере нет ничего плохого. От std::vector::push_back:

Если конструктором перемещения T не является noexcept и T не является CopyInsertable в *this, вектор будет использовать конструктор броска перемещения. Если он выбрасывает, гарантия отменяется, а последствия не уточняются.

std::vector предпочитает не перемещающие конструкторы перемещения, и если ни один из них не доступен, прибегнет к конструктору копирования (выбрасывающему или нет). Но если это также не доступно, тогда он должен использовать конструктор броска. По сути, вектор пытается уберечь вас от бросания конструкторов и оставления объектов в неопределенном состоянии.

Таким образом, в этом отношении ваш пример верен, но если ваш конструктор перемещения на самом деле выдает исключение, то у вас будет неопределенное поведение.

4
Rakete1111 22 Окт 2017 в 13:11