Почему std::vector копирует конструкцию вместо конструкции перемещения, когда деструктор может выбросить?

Рассмотрим следующую программу:

#include <vector>
#include <iostream>

class A {
    int x;
public:
    A(int n)          noexcept : x(n)       { std::cout << "ctor with value\n"; }
    A(const A& other) noexcept : x(other.x) { std::cout << "copy ctor\n"; }
    A(A&& other)      noexcept : x(other.x) { std::cout << "move ctor\n"; }
    ~A()                                    { std::cout << "dtor\n"; } // (*)
};

int main()
{
    std::vector<A> v;
    v.emplace_back(123);
    v.emplace_back(456);
}

Если я запускаю программу, я получаю (GodBolt):

ctor with value
ctor with value
move ctor
dtor
dtor
dtor

... что соответствует тому, что я ожидал. Однако, если в строке (*) я помечаю деструктор как потенциально бросающий, я тогда получаю :

ctor with value
ctor with value
copy ctor
dtor
dtor
dtor

... т.е. копирующий ctor используется вместо move ctor. Почему это так? Не похоже, что копирование предотвращает разрушения, которые потребовались бы при перемещении.

Связанные вопросы:

@JasonLiam, хотя и связан, определенно не обманщик. Суть этого ответа в том, что конструктор копирования выбран, потому что деструктор не помечен noexcept. Этот вопрос спрашивает, почему выбран конструктор копирования, если деструктор может генерировать исключения.

Revolver_Ocelot 10.10.2022 11:44

Связанный дубликат — это ошибка в старой версии GCC, связанная со случаем, когда деструктор без спецификатора noexcept ведет себя следующим образом. Здесь вопрос касается случая со спецификатором noexcept. Так что я снова откроюсь.

user17732522 10.10.2022 11:45

Две недавние записи в блоге О'Дуайера актуальны и хорошо читаются: Что такое «векторная пессимизация»? и продолжение Треугольник «выбери два» для std::vector.

davidbak 10.10.2022 22:19
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
34
4
1 134
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это LWG2116. Выбор между перемещением и копированием элементов часто выражается как std::is_nothrow_move_constructible, т.е. noexcept(T(T&&)), что также ошибочно проверяет деструктор.

Разве это не должно быть noexcept(T(T&&)) || !noexcept(T(T)) тогда?

einpoklum 10.10.2022 11:33

@einpoklum нет, проблема в том, что он вообще не должен проверять деструктор, потому что, если это не удастся, вы не сможете вернуться к тому, с чего началось.

Caleth 10.10.2022 11:40

Однако я не вижу в стандарте ничего, что требовало бы от реализации std::vector фактического использования std::is_nothrow_move_constructible для принятия решения о копировании/перемещении. Гарантии безопасности исключений указаны в его терминах, но более слабая проверка только на конструкторе все равно удовлетворила бы их, я думаю. (eel.is/c++draft/containers#vector.modifiers-2)

user17732522 10.10.2022 11:54

@user17732522 user17732522 дело в том, что некоторые реализации используют is_nothrow_move_constructible или что-то с эквивалентной чрезмерной проверкой

Caleth 10.10.2022 11:57

Думаю, это имеет смысл. Использование типа с потенциально генерирующим деструктором в стандартном библиотечном контейнере уже необычно. Они также не позволяют на самом деле бросать из деструктора.

user17732522 10.10.2022 12:03

@ user17732522: Это все еще не объясняет, почему копирование предпочтительнее перемещения (в отличие от полного отклонения типа).

einpoklum 10.10.2022 16:03

@einpoklum Отклонение запрещено. std::vector должен принимать типы MoveInsertible и может использовать конструкцию копирования, если это возможно. Независимо от того, выбрасывает ли конструкция перемещения, это не имеет значения. Также не имеет значения, является ли деструктор потенциально генерирующим. Предварительное условие состоит в том, что деструктор не может на самом деле генерировать, что компилятор не может проверить.

user17732522 10.10.2022 16:08

@user17732522 user17732522 Я предполагаю, что путаница einpoklum (или, по крайней мере, моя) заключается в том, почему любая реализация предпочтет копирование вместо перемещения, учитывая, что вам нужно вызывать деструктор в любом случае. (И потенциально генерирующий деструктор, если вы используете конструктор копирования, с большей вероятностью будет генерировать выброс.)

benrg 10.10.2022 19:39

@benrg Реализация не должна заботиться о том, вызывает ли деструктор исключение, потому что это предварительное условие для пользователя библиотеки, чтобы этого не произошло. Однако реализация должна гарантировать, что если std::is_nothrow_move_constructible имеет значение false, но тип по-прежнему CopyInsertable, то любое исключение, выброшенное из конструктора (ов), не приведет к нарушению гарантий исключений, то есть вектор должен оставаться в исходном состоянии. Как правило, невозможно гарантировать, что перемещение использовалось до/во время возникновения исключения. Так что если ход может бросить, то надо использовать копию.

user17732522 10.10.2022 19:52

@benrg Тогда кажется, что реализации решили просто использовать упомянутую черту типа (которая, вероятно, сама по себе является дефектом для рассмотрения деструктора) напрямую из описания гарантии безопасности исключений, не оптимизируя этот очень необычный случай потенциального выброса , но на самом деле не вызывает деструктор класса с конструктором noexcept перемещения.

user17732522 10.10.2022 19:53

@MartinYork конструктор перемещения не исключение, а деструктор - нет. Выбор копирования или перемещения не имеет значения для выбрасывающего деструктора, потому что к этому моменту вы закончили жизнь некоторых элементов.

Caleth 11.10.2022 22:14
Ответ принят как подходящий

Вкратце: Потому что std::vector предпочитает предлагать вам «надежную гарантию исключения».

(Thanks goes to Jonathan Wakely, @davidbak, @Caleth for links & explanations)

Предположим, что в вашем случае std::vector нужно было использовать конструкцию перемещения; и предположим, что во время изменения размера вектора одним из вызовов A::~A должно быть выдано исключение. В этом случае у вас будет непригодный для использования std::vector, частично перемещенный.

С другой стороны, если std::vector выполняет построение копии, и в одном из деструкторов возникает исключение — он может просто выкинуть новую копию, и ваш вектор будет в том же состоянии, что и до изменения размера. Это «сильная гарантия исключения» для объекта std::vector.

Разработчики стандартной библиотеки предпочли эту гарантию оптимизации производительности изменения размера векторов.

Об этом сообщалось как о проблеме/дефекте стандартной библиотеки (LWG 2116), но после некоторого обсуждения было решено сохранить текущее поведение в соответствии с приведенным выше соображением.

См. также сообщение Артура О'Двира: Треугольник "Выберите любые два" для std::vector.

Действительно ли уместна здесь строгая гарантия исключения? Все разрушения (могут) происходить после перемещений, поэтому перемещения предположительно завершены, и новое содержимое vector может быть заблокировано (вы просто получаете исключение, очищающее старые данные). Даже если вы выполняете копирование, те же исключения могут быть вызваны, когда вы очищаете старые данные, из которых вы скопировали. Нет никакого морального различия между «скопировано все содержимое, и произошло исключение, очищающее старый немодифицированный материал» и «перемещено все содержимое, и произошло исключение, очистившее пустое содержимое».

ShadowRanger 11.10.2022 02:52

@MartinYork: Вопрос касается выбрасывания деструкторов, а не перемещения или копирования конструкторов. Все вызовы деструктора могут быть объединены в пакеты после завершения перемещения/копирования, поэтому строгая гарантия исключения здесь кажется неуместной: «транзакция» завершена к моменту очистки.

Matthieu M. 11.10.2022 09:33

@ShadowRanger: Это интересный момент. Я полагаю, что для проектировщиков библиотек уничтожение является частью операции. Но я понимаю, что вы имеете в виду.

einpoklum 11.10.2022 15:25

Вызов A::~A на самом деле броска не допускается. Контейнеры стандартных библиотек не поддерживают такие типы. Вы также можете посмотреть на реализации. Я не видел реализации вызовов деструктора libstdc++, libc++ или MS Guard с обработчиками исключений. Если деструктор выдает исключение, оно, скорее всего, просто распространится на пользователя и оставит вектор в несогласованном состоянии. Так что это не может быть действительно актуально.

user17732522 15.10.2022 01:47

Гарантия строгого исключения актуальна только тогда, когда конструктор перемещения вызывает исключение (что разрешено для типов, используемых в стандартных контейнерах). Просто описание гарантии в стандарте также зависит от спецификации исключения деструктора, что на самом деле не имеет смысла, поскольку предполагается, что деструктор в любом случае не выбрасывает.

user17732522 15.10.2022 01:49

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