Рассмотрим следующую программу:
#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. Этот вопрос спрашивает, почему выбран конструктор копирования, если деструктор может генерировать исключения.
Связанный дубликат — это ошибка в старой версии GCC, связанная со случаем, когда деструктор без спецификатора noexcept ведет себя следующим образом. Здесь вопрос касается случая со спецификатором noexcept. Так что я снова откроюсь.
Две недавние записи в блоге О'Дуайера актуальны и хорошо читаются: Что такое «векторная пессимизация»? и продолжение Треугольник «выбери два» для std::vector.





Это LWG2116. Выбор между перемещением и копированием элементов часто выражается как std::is_nothrow_move_constructible, т.е. noexcept(T(T&&)), что также ошибочно проверяет деструктор.
Разве это не должно быть noexcept(T(T&&)) || !noexcept(T(T)) тогда?
@einpoklum нет, проблема в том, что он вообще не должен проверять деструктор, потому что, если это не удастся, вы не сможете вернуться к тому, с чего началось.
Однако я не вижу в стандарте ничего, что требовало бы от реализации std::vector фактического использования std::is_nothrow_move_constructible для принятия решения о копировании/перемещении. Гарантии безопасности исключений указаны в его терминах, но более слабая проверка только на конструкторе все равно удовлетворила бы их, я думаю. (eel.is/c++draft/containers#vector.modifiers-2)
@user17732522 user17732522 дело в том, что некоторые реализации используют is_nothrow_move_constructible или что-то с эквивалентной чрезмерной проверкой
Думаю, это имеет смысл. Использование типа с потенциально генерирующим деструктором в стандартном библиотечном контейнере уже необычно. Они также не позволяют на самом деле бросать из деструктора.
@ user17732522: Это все еще не объясняет, почему копирование предпочтительнее перемещения (в отличие от полного отклонения типа).
@einpoklum Отклонение запрещено. std::vector должен принимать типы MoveInsertible и может использовать конструкцию копирования, если это возможно. Независимо от того, выбрасывает ли конструкция перемещения, это не имеет значения. Также не имеет значения, является ли деструктор потенциально генерирующим. Предварительное условие состоит в том, что деструктор не может на самом деле генерировать, что компилятор не может проверить.
@user17732522 user17732522 Я предполагаю, что путаница einpoklum (или, по крайней мере, моя) заключается в том, почему любая реализация предпочтет копирование вместо перемещения, учитывая, что вам нужно вызывать деструктор в любом случае. (И потенциально генерирующий деструктор, если вы используете конструктор копирования, с большей вероятностью будет генерировать выброс.)
@benrg Реализация не должна заботиться о том, вызывает ли деструктор исключение, потому что это предварительное условие для пользователя библиотеки, чтобы этого не произошло. Однако реализация должна гарантировать, что если std::is_nothrow_move_constructible имеет значение false, но тип по-прежнему CopyInsertable, то любое исключение, выброшенное из конструктора (ов), не приведет к нарушению гарантий исключений, то есть вектор должен оставаться в исходном состоянии. Как правило, невозможно гарантировать, что перемещение использовалось до/во время возникновения исключения. Так что если ход может бросить, то надо использовать копию.
@benrg Тогда кажется, что реализации решили просто использовать упомянутую черту типа (которая, вероятно, сама по себе является дефектом для рассмотрения деструктора) напрямую из описания гарантии безопасности исключений, не оптимизируя этот очень необычный случай потенциального выброса , но на самом деле не вызывает деструктор класса с конструктором noexcept перемещения.
@MartinYork конструктор перемещения не исключение, а деструктор - нет. Выбор копирования или перемещения не имеет значения для выбрасывающего деструктора, потому что к этому моменту вы закончили жизнь некоторых элементов.
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 может быть заблокировано (вы просто получаете исключение, очищающее старые данные). Даже если вы выполняете копирование, те же исключения могут быть вызваны, когда вы очищаете старые данные, из которых вы скопировали. Нет никакого морального различия между «скопировано все содержимое, и произошло исключение, очищающее старый немодифицированный материал» и «перемещено все содержимое, и произошло исключение, очистившее пустое содержимое».
@MartinYork: Вопрос касается выбрасывания деструкторов, а не перемещения или копирования конструкторов. Все вызовы деструктора могут быть объединены в пакеты после завершения перемещения/копирования, поэтому строгая гарантия исключения здесь кажется неуместной: «транзакция» завершена к моменту очистки.
@ShadowRanger: Это интересный момент. Я полагаю, что для проектировщиков библиотек уничтожение является частью операции. Но я понимаю, что вы имеете в виду.
Вызов A::~A на самом деле броска не допускается. Контейнеры стандартных библиотек не поддерживают такие типы. Вы также можете посмотреть на реализации. Я не видел реализации вызовов деструктора libstdc++, libc++ или MS Guard с обработчиками исключений. Если деструктор выдает исключение, оно, скорее всего, просто распространится на пользователя и оставит вектор в несогласованном состоянии. Так что это не может быть действительно актуально.
Гарантия строгого исключения актуальна только тогда, когда конструктор перемещения вызывает исключение (что разрешено для типов, используемых в стандартных контейнерах). Просто описание гарантии в стандарте также зависит от спецификации исключения деструктора, что на самом деле не имеет смысла, поскольку предполагается, что деструктор в любом случае не выбрасывает.
Связанный/Дуп: Перераспределение векторов использует копирование вместо конструктора перемещения.