Перемещаемые, но не скопируемые исключения

Я думаю о написании классов, не подлежащих копированию. Я нахожу это интересным, потому что тогда мне не нужно беспокоиться об исключениях, которые могут быть выбраны при распределении внутри экземпляра-копии. Если создание объекта исключения завершается успешно, все в порядке и не должно быть проблем с std::terminate.

struct exception
{
    exception() = default;
    exception(const exception&) = delete;
    exception(exception&&) noexcept = default;
    ~exception() noexcept = default;
    auto operator=(const exception&) -> exception& = delete;
    auto operator=(exception&&) noexcept -> exception& = delete;
};

int main()
{
    try {
        try {
            throw exception{};
        } catch (...) {
            std::rethrow_exception(
                std::current_exception());
        }
    } catch (const exception& e) {
        return 1;
    }
}

GCC-4.7 и Clang-3.2 принимают вышеуказанный код. Однако я немного удивлен. Насколько я знаю, существует несколько ситуаций, когда объекты исключений могут быть скопированы, например. std::current_exception() и std::rethrow_exception().

Вопрос: Является ли приведенный выше код правильным в соответствии с С++ 11, то есть он будет принят всеми компиляторами, соответствующими С++ 11?

Отредактировано: Добавлены std::rethrow_exception и std::current_exception к примеру. Оба компилятора принимают эту версию. Это должно дать понять, что если компилятор не требует конструктора-копии, когда генерируется исключение, компилятор не будет требовать его при использовании этих двух функций.

Ответ 1

current_exception говорит, что он относится либо к текущему исключению, либо к его копии, но не говорит, что. Это говорит мне, что:

  • не указано, скопировано ли оно или нет. [*]
  • поэтому ваш класс исключений не является хорошим (конечно, если кто-то не может называть его current_exception)
  • также поэтому не удивительно, что он работает в некоторых реализациях. Вероятно, не было бы условий для того, чтобы текущее исключение не было скопировано, если бы не было спроса на это от разработчиков или надеемся, что разработчики избежали бы копий.

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

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

[*] Это явно не указано, создает ли или нет current_exception "новую копию каждый раз, когда она вызывается", но я не полностью уверен, подразумевается ли это, что она не указана, создает ли она новую копию первой время, которое оно вызывало.

Ответ 2

Однако я немного удивлен. Насколько я знаю, существует несколько ситуаций, когда объекты исключений могут быть скопированы, например. std::current_exception() и std::rethrow_exception().

Но вы не вызываете никого из них. В стандарте совершенно ясно, как инициализируется объект исключения. Из 15.1, p3:

Выражение-выражение инициализирует временный объект, называемый объектом исключения, тип которого определяется удалением любого cv-квалификатора верхнего уровня s из статического типа операнда броска и корректировки типа из "массива T" или "функция возвращает T" в "указатель на T" или "указатель на функцию возврата T" соответственно. Временной является значением lvalue и используется для инициализации переменной, указанной в соответствующем обработчике (15.3). Если тип объекта исключения будет неполным или указатель на неполный тип, отличный от (возможно, cv-qualified), то программа будет плохо сформирована. За исключением этих ограничений и ограничений на сопоставление типов, упомянутых в 15.3, операнд throw обрабатывается точно как аргумент функции в вызов (5.2.2) или операнд оператора return.

Короче говоря, он действует как возврат типа по значению: объект возвращаемого значения/исключения инициализируется выражением, которое вы предоставляете. Поскольку выражение, которое вы используете, является временным, оно будет действовать как возвращение временного из функции и вызов конструктора перемещения. Конечно, шансы на то, что это будет устранено, но что точка 15.1, p5 делает:

Когда брошенный объект является объектом класса, конструктор copy/move и деструктор должны быть доступны, даже если операция копирования/перемещения завершена (12.8).

Это справедливо для возвращаемых значений: возвращаемое значение инициализируется путем инициализации копирования/перемещения, где это необходимо. Таким образом, подходящие конструкторы должны быть доступны, даже если они удалены.

Вы не можете бросить свой класс исключений таким образом, чтобы потребовалось бы копирование объекта исключения. Таким образом, вы не можете бросать lvalue; вы можете только выдать значение prvalue или xvalue.

Нигде в стандарте не говорится, что системе разрешено произвольно копировать исключение без причины. Вызов std::current_exception может скопировать его. Вызов std::rethrow_exception скорее всего скопирует его.

Но если вы не назовете объекты, которые явно копируют ваши объекты исключений, С++ не разрешается делать это волей-неволей.