Рассмотрим этот код:
#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
, упомянутых там.
Побочный вопрос, не связанный с языковым юристом : почему такое поведение такое, какое оно есть? Это дизайнерский выбор? Или это дизайнерская необходимость?
@RichardCritten throw;
работает только тогда, когда вы в данный момент обрабатываете исключение (т. е. вы находитесь в catch
теле). Здесь e1
— это просто переданный объект исключения, он не обрабатывается.
Что касается необходимости, то выбрасываемый объект исключения должен находиться где-то кроме стека, который собирается разматывать. Таким образом, реализация не может просто взять ссылку на то, что было в прошлом, она должна сделать копию. В противном случае это было бы довольно большим бременем.
Кроме того, стандарту не нужно утруждать себя предупреждением о нарезке повсюду. Нарезка ВСЕГДА происходит, когда производный объект копируется в объект базового класса, независимо от контекста. И хотя я не знаю, упоминает ли вообще стандарт C++ о нарезке, в этом совершенно нет необходимости, поскольку это совершенно очевидно: когда вы копируете объект производного класса в объект базового класса, вы получаете объект базового класса как результат. Что еще вы получите? C++ делает очень мало скрытой магии.
Нарезка происходит, когда вы генерируете исключение 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 не могут иметь ссылочный тип, decltype(e1)
может быть ссылочным типом
Да, но если бы я передал выражение e1
функции, принимающей std::exception const& e2
, имя e2
в этой функции все равно будет ссылаться на тот же объект e1
, на который ссылаются. Это не то, что происходит во фрагменте моего вопроса, где catch
«играет роль функции». Или, говоря языком кода, почему бы мне не прочитать 3 раза один и тот же адрес здесь?
@Enlico «Тип объекта исключения определяется путем удаления всех квалификаторов cv верхнего уровня из типа операнда». Тип операнда — std::exception
(нет ссылки, поскольку тип выражения никогда не является ссылкой). При инициализации аргумента функции учитывается тип, а также категория значения выражения.
Кроме того, когда catch «играет роль функции»: catch привязывает ссылку к объекту исключения, но объект исключения является копией объекта, переданного в throw, поэтому вы не ожидаете, что адрес будет равен.
Во-первых, обратите внимание, что существует объект, контролируемый реализацией и не указанный явно в исходном коде:
Вызов исключения инициализирует объект с динамическим сроком хранения, называемый объектом исключения. Если тип объекта исключения будет неполным типом ([basic.types.general]), типом абстрактного класса ([class.abstract]) или указателем на неполный тип, отличный от cv void ([basic.compound ]) программа неправильно сформирована.
Этот объект исключения будет инициализирован выражением throw throw e1
и является объектом, к которому будет привязан обработчик. По этой причине не стоит ожидать трех копий одного и того же адреса в вашем примере здесь. Первые два — это адреса объекта, передаваемого в throw, а третий — адрес объекта исключения. И они будут разными, потому что это разные объекты.
Каков тип объекта исключения?
Выражение 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
, потому что ссылка вообще не будет объектом. Спасибо!
То, что вы создаете, - это подобъект базового класса, вы не можете создавать ссылки, поэтому создается копия этого подобъекта базового класса. То же самое будет, если вы передадите подобъект базового класса в функцию, которая принимает данные по значению.