Где из стандарта я прочитал, что исключения производного класса, хранящиеся по ссылке на базовый класс, отсекаются при обнаружении?

Рассмотрим этот код:

#include <iostream>
#include <vector>
#include <exception>

void foo(std::exception const& e1) {
    try {
        std::cout << e1.what() << std::endl;
        throw e1;
    } catch (std::exception const& e2) {
        std::cout << e2.what() << std::endl;
    }
}

int main() {
    foo(std::runtime_error{"this is not std::exception"});
}

Он напечатает следующее

this is not std::exception
std::exception

где вторая строка, если я правильно понимаю, не предусмотрена стандартом.

В любом случае, код, из которого я выделил приведенный выше фрагмент, — это то, как я обнаружил, что, несмотря на то, что throw e1;e1 является объектом производного класса (даже если он удерживается через ссылку на базовый класс), он разрезается до того, как достигнет обработчика.

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

Я думаю это предложение вероятно актуально:

Объект исключения инициализируется копированием ([dcl.init.general]) из операнда (возможно, преобразованного).

Означает ли инициализация копирования, что «объект», привязанный к e в строке catch, инициализируется следующим образом

auto entityToWhich_e2_isBound = e1;

вместо этого?

auto& entityToWhich_e2_isBound = e1;

Если я перейду по ссылке на [dcl.init.general], я найду 14, где я это прочитал

Инициализация, которая происходит при […] выдаче исключения ([Exception.throw] ), обработке исключения ( [Exception.handle]) […] называется копированием-инициализацией.

Но я не совсем понимаю, как это говорит мне о нарезке.

В моем примере e1 имеет статический тип (std::exception) и динамический тип (std::runtime_error), верно? Поэтому я не знаю точно, как читать §14.4 относительно типов E и T, упомянутых там.


Побочный вопрос, не связанный с языковым юристом : почему такое поведение такое, какое оно есть? Это дизайнерский выбор? Или это дизайнерская необходимость?

То, что вы создаете, - это подобъект базового класса, вы не можете создавать ссылки, поэтому создается копия этого подобъекта базового класса. То же самое будет, если вы передадите подобъект базового класса в функцию, которая принимает данные по значению.

Öö Tiib 14.06.2024 12:13

@RichardCritten throw; работает только тогда, когда вы в данный момент обрабатываете исключение (т. е. вы находитесь в catch теле). Здесь e1 — это просто переданный объект исключения, он не обрабатывается.

Yksisarvinen 14.06.2024 12:36

Что касается необходимости, то выбрасываемый объект исключения должен находиться где-то кроме стека, который собирается разматывать. Таким образом, реализация не может просто взять ссылку на то, что было в прошлом, она должна сделать копию. В противном случае это было бы довольно большим бременем.

Jeff Garrett 14.06.2024 15:41

Кроме того, стандарту не нужно утруждать себя предупреждением о нарезке повсюду. Нарезка ВСЕГДА происходит, когда производный объект копируется в объект базового класса, независимо от контекста. И хотя я не знаю, упоминает ли вообще стандарт C++ о нарезке, в этом совершенно нет необходимости, поскольку это совершенно очевидно: когда вы копируете объект производного класса в объект базового класса, вы получаете объект базового класса как результат. Что еще вы получите? C++ делает очень мало скрытой магии.

Christian Stieber 14.06.2024 16:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
4
553
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Нарезка происходит, когда вы генерируете исключение https://eel.is/c++draft/expr.throw#2

Тип объекта исключения определяется путем удаления всех cv-квалификаторов верхнего уровня из типа (возможно, преобразованного) операнда. Объект исключения инициализируется копированием из (возможно, преобразованного) операнда.

Как будто да throw std::decay_t<decltype(e1)>(e1);

Это необходимо, поскольку новый объект должен быть создан везде, где хранится объект исключения, поэтому он может выделить любое количество байтов только для объявленного типа объекта и может вызвать только конструктор перемещения объявленного типа объекта (как бы он знает, как вызвать конструктор перемещения runtime_error?)

Текст, который вы процитировали, — это абзац, из которого я процитировал предложение, поэтому я его прочитал. И я знаю, что означает «удаление всех cv-квалификаторов верхнего уровня из типа», но это только оправдывает часть std::remove_cv в std::decay. А как насчет std::remove_reference части? Это означает, что «объект инициализируется копированием»?

Enlico 14.06.2024 13:40

Выражения @Enlico не могут иметь ссылочный тип, decltype(e1) может быть ссылочным типом

Artyer 14.06.2024 13:45

Да, но если бы я передал выражение e1 функции, принимающей std::exception const& e2, имя e2 в этой функции все равно будет ссылаться на тот же объект e1, на который ссылаются. Это не то, что происходит во фрагменте моего вопроса, где catch «играет роль функции». Или, говоря языком кода, почему бы мне не прочитать 3 раза один и тот же адрес здесь?

Enlico 14.06.2024 14:37

@Enlico «Тип объекта исключения определяется путем удаления всех квалификаторов cv верхнего уровня из типа операнда». Тип операнда — std::exception (нет ссылки, поскольку тип выражения никогда не является ссылкой). При инициализации аргумента функции учитывается тип, а также категория значения выражения.

Artyer 14.06.2024 15:53

Кроме того, когда catch «играет роль функции»: catch привязывает ссылку к объекту исключения, но объект исключения является копией объекта, переданного в throw, поэтому вы не ожидаете, что адрес будет равен.

Jeff Garrett 14.06.2024 16:43
Ответ принят как подходящий

Во-первых, обратите внимание, что существует объект, контролируемый реализацией и не указанный явно в исходном коде:

[кроме.throw/3]

Вызов исключения инициализирует объект с динамическим сроком хранения, называемый объектом исключения. Если тип объекта исключения будет неполным типом ([basic.types.general]), типом абстрактного класса ([class.abstract]) или указателем на неполный тип, отличный от cv void ([basic.compound ]) программа неправильно сформирована.

Этот объект исключения будет инициализирован выражением throw throw e1 и является объектом, к которому будет привязан обработчик. По этой причине не стоит ожидать трех копий одного и того же адреса в вашем примере здесь. Первые два — это адреса объекта, передаваемого в throw, а третий — адрес объекта исключения. И они будут разными, потому что это разные объекты.

Каков тип объекта исключения?

[expr.throw/2]

Выражение throw с операндом генерирует исключение ([кроме.throw]). Над операндом выполняются стандартные преобразования массива в указатель ([conv.array]) и функции в указатель ([conv.func]). Тип объекта исключения определяется путем удаления всех cv-квалификаторов верхнего уровня из типа (возможно, преобразованного) операнда. Объект исключения инициализируется копированием ([dcl.init.general]) из операнда (возможно, преобразованного).

Тип выражения e1 (операнд выражения throw) — const std::exception. Правила здесь , но выражения никогда не имеют ссылочного типа. Вот что вы получите от:

std::remove_reference_t<decltype((e1))>

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

Итак... операнд имеет тип const std::exception. Мы проверяем преобразования массива в указатель и функции в указатель, но ни одно из них не применяется. Таким образом, тип объекта исключения определяется путем удаления всех cv-квалификаторов верхнего уровня. Таким образом, тип объекта исключения — std::exception.

Объект типа std::exception инициализируется копированием из операнда. Поэтому это как если бы мы написали:

std::exception exception_object = e1;

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

вторая строка, если я правильно понимаю, не предусмотрена стандартом

Строка, возвращаемая std::exception::what(), определяется реализацией.

В моем примере e1 имеет статический тип (std::Exception) и динамический тип (std::runtime_error), верно? Поэтому я не знаю точно, как читать §14.4 относительно упомянутых там типов E и T.

E — это тип объекта исключения, который имеет статический и динамический тип std::exception.

Побочный вопрос, не связанный с языковым юристом: почему такое поведение такое, какое оно есть? Это дизайнерский выбор? Или это дизайнерская необходимость?

Как я заметил в комментарии, причина этого естественна в том, что объект должен быть "на стороне", а не в стеке, потому что мы разматываемся. В C++ мы создаем новый объект из существующего путем копирования.

Изначально я ожидал (еще до того, как сформулировать вопрос), что эти 3 адреса будут одинаковыми. Но спасибо, теперь я понимаю, что основная мысль, которую я пропустил, - это то, что выдача исключения инициализирует объект, поэтому объект исключения не может быть ссылкой на операнд throw, потому что ссылка вообще не будет объектом. Спасибо!

Enlico 14.06.2024 17:27

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