Предположим, у меня есть функция, возвращающая значение.
S f();
и я называю это так:
auto s = f();
Если копия-конструктор S
выбрасывает и return some_s;
внутри f
окружен блоком try-catch,
гарантировано ли, что исключение будет перехвачено
внутри f
, а не кинут на место звонка?
Я понимаю, что auto s = f();
подлежит обязательному удалению копирования, начиная с C++17, поэтому существует (максимум) один вызов конструктора копирования S
. Но гарантированно ли это произойдет внутри f
с точки зрения обработки исключений?
Тесты в gcc, clang, mingw и msvc показывают, что это действительно так (код ниже). Меня беспокоит то, что
В любом случае некоторые цитаты из стандарта будут признательны ;)
Вот код, который я использовал для тестирования
#include<iostream>
struct S{
S(const S &){
throw "Oops";
}
S(int) noexcept {}
};
S f(){
static/*to avoid RVO*/ S stat(7);
try{
return stat;
}catch(const char *what){
std::cerr << "still in function after failed-return" << std::endl;
}
return S(7);// mandatory copy-elision (c++17);
}
int main()
try{
auto s = f();// mandatory copy-elision (c++17);
}catch(...){
std::cerr << "Exception from main..." << std::endl;
}
@user12002570 user12002570 Спасибо, как сказано в вопросе, я это знаю. Но означает ли это, что на месте вызова не будет создано никаких исключений?
@user12002570 f()
возвращает stat
, который будет использовать конструктор копирования, поэтому копирующий вектор не может быть delete
d.
@TedLyngmo Да, return stat;
здесь будет использоваться копировальный аппарат. Я имел в виду return S(7)
. Спецификацию проверю.
Интересный крайний случай: функциональные блоки try. Все тело функции может представлять собой блок try-catch.
@MSalters Да, это блокировка функции
Начиная с C++17, после возврата f
копия отсутствует. Возвращаемое значение f
— это нематериализованное временное значение, которое материализуется при инициализации s
, оно не является объектом и его не нужно копировать для инициализации чего-либо.
Это не «обязательное удаление копии». Правила существования были изменены таким образом, что объекта (пока) нет.
кажется странным, что return на самом деле не передает управление из функции
В операторе return
могут быть сколь угодно сложные подвыражения, которые могут выдавать ошибки. Если да, то вы можете перехватить их внутри функции. Было бы крайне неудобно, если бы вы не могли этого сделать.
Спасибо. Если g()
вбрасывает return g();
, мне ясно, что это происходит до фактического возврата. Здесь я спрашиваю, в какой именно момент в моем коде вызывается конструктор копирования (что касается обработки исключений).
@Адриан, помни, что stat
— это выражение, и оно должно быть преобразовано из lvalue в rvalue, чтобы быть S
вместо S&
.
@ 463035818_is_not_an_ai В МОЕМ коде ЕСТЬ копия. Вы изменили мой return stat;
на возвращаемое значение.
f()
нет РВО. Он возвращает именованный статический объект и должен использовать конструктор копирования (или конструктор перемещения, если бы он существовал).
@Калет, еще раз спасибо, но я все еще не уверен. Вы говорите: «f
возвращает нематериализованное временное значение, которое материализуется при инициализации s
». Кажется, это подразумевает, что инициализация (т. е. построение копирования) происходит ПОСЛЕ возврата функции, а не внутри try
блока f
.
@Адриан, после возвращения f
копии не будет. Значение, возвращаемое f
, не является объектом до тех пор, пока оно кому-то не понадобится, в данном случае это инициализация s
. Переход C++14 -> C++17 приводит к серьезным изменениям в поведении абстрактной машины (которую описывает стандарт), даже если реализации остались в основном прежними.
@Калет Хорошо, значит, правило № 12 из en.cppreference.com/w/cpp/language/eval_order#Rules все еще применимо здесь, верно? Вы говорите, что конструктор копирования гарантированно вызывается «внутри» f
? Из статьи об исключении копирования на cppreference это неясно.
@Адриан, тебе это не нужно. Граммер — это обработчик-seq составного оператора, который сопоставляет любое исключение, выданное составным оператором, с обработчиками в handler-seq. В try
есть копия, это один из случаев инициализации копирования. Формулировка cppreference немного вводит в заблуждение, поскольку говорит об инициализации объектов, что не подходит для случая return stat
.
@Caleth Но откуда вы знаете, что «есть копия в возвращаемой статистике»? Я считаю, что вы правы, но как это следует из стандарта? Я могу поверить, что правила C++ 17 гласят, что вызывается конструктор копирования один раз (без RVO, как в моем случае), поэтому моя проблема сводится к вопросу, как мы узнаем, что он вызывается в ответном stat.
@Адриан Кажется, ты отклонился от заданного вопроса. Я думаю, вам следует посмотреть, получили ли вы ответ на вопрос в каком-либо из полученных ответов, а затем, возможно, задать новый вопрос о том, когда NRVO разрешено, а когда RVO обязательно. Копирование Elision @ cppreference может быть хорошим началом. Вы обнаружите, что NRVO (в return stat;
) не разрешен. Поскольку это запрещено, его необходимо скопировать.
@Adrian По сути, это особое поведение prvalues. Начиная с C++17, они больше не являются «временными значениями», а являются «рецептами инициализации». По сути, последовательность «копий» prvalue исключается так, что пункт назначения на одном конце последовательности (s
) инициализируется непосредственно конструкцией, которая «инициализировала» prvalue на другом конце последовательности (конструктор копирования в return stat
). .
Я считаю, что на этот вопрос можно ответить без языкового юриста.
X f() {return ...;}
Y y = f();
Обычно это предполагает две копии (или, скорее, создание двух объектов), если не опущено.
Сначала ...
копируется в возвращаемый временный объект (типа X
), а затем этот временный объект копируется в Y y
.
Второй всегда опускается, если X
и Y
имеют один и тот же тип (начиная с C++17) (в том числе, если Y
является auto
). Первый из них пропускается, если вы возвращаете значение prvalue.
Если первый выдает ошибку, функция может его перехватить. Если второй выкинет, поймать его сможет только вызывающий абонент.
Это имеет смысл, поскольку первое копирование происходит относительно рано, еще до того, как локальные переменные будут уничтожены. Поэтому было бы мало смысла запрещать функции перехватывать исключения во время этого копирования, но затем разрешать перехват исключений из деструкторов локальных переменных, которые могут быть выброшены после этого.
Спасибо. Вы имеете в виду № 12 из en.cppreference.com/w/cpp/language/eval_order#Rules, и мне действительно было интересно, гарантирует ли это, что исключение будет перехвачено внутри функции. Я думаю, что правила С++ 17 делают это еще более запутанным (по крайней мере, для меня).
гарантированно ли это произойдет внутри
f
с точки зрения обработки исключений?
Да.
18.1. Вызов исключения
- При возникновении исключения управление передается ближайшему обработчику соответствующего типа ([Exception.handle] ); «ближайший» означает обработчик, для которого составной оператор или ctor-инициализатор, следующий за ключевым словом
try
, был последним введен потоком управления и еще не завершился.
Когда конструктор копирования добавляет ваш пример, блок try
в f()
еще не закрыт. Исключение возникает до завершения вычисления полного выражения в операторе return
.
Это означает, что управление передается ближайшему обработчику соответствующего типа, который также находится внутри f()
.
Спасибо! Что меня все еще беспокоит, так это то, как это взаимодействует с обязательным «копированием» операнда в операторе возврата С++ 17.
@Adrian В return stat;
невозможно NRVO (называемое оптимизацией возвращаемого значения, ранее копированием), а тем более RVO (ранее называвшееся обязательным исключением копирования).
Да, именно поэтому я сделал его статическим. Я должен подумать об этом. Спасибо.
@Адриан Адриан Всегда пожалуйста! Я надеюсь, что отрывок из стандарта в конечном итоге прояснит, что исключение гарантированно будет перехвачено функцией (если для него установлен обработчик, как в вашем случае). Если убрать сложность занятий, я думаю, за этим будет легче следить. пример
Конструктор копирования, работающий в return stat;
, инициализирует переменную s
в auto s = f();
. Несмотря на такой долгосрочный эффект вне функции, он по-прежнему выполняется внутри функции как часть оператора return
. Следовательно, его исключения могут быть перехвачены функцией.
Да, действительно, я забыл обратиться к @Adrian.
@j6t Я думаю, ты прав. И вы обращаетесь именно к моей проблеме! Но я пытаюсь соединить точки: как именно это следует из правил языка? Откуда вы знаете, что «конструктор копирования запускается в return stat» ввиду правил C++17 о «исключении копирования»?
@Adrian В return stat;
нет копирования. Каждое полное выражение упорядочивается перед следующим полным выражением, поэтому return
expression
;
необходимо полностью вычислить перед передачей управления куда-либо. Раз бросает, то контроль пока в пределах f()
.
@Адриан, мы знаем, потому что stat
— это выражение lvalue, обозначающее объект в статическом хранилище, поэтому return stat
соответствует инициализации копирования правила грамматики.
@Caleth Я не понимаю, как это подразумевает стандартный текст об инициализации копирования. Но я обнаружил, что примечание в timsong-cpp.github.io/cppwp/n4659/stmt.return#2 явно говорит: «Оператор return может включать вызов конструктора для выполнения копирования или перемещения». Вы знаете, где в стандарте описана вся эта «материализация». Текст в /n4659/class.temporary не особо информативен.
@Адриан, это происходит из определения категорий значений Basic.lval
@Адриан «Временная материализация» — это противоположность тому, что происходит здесь.
Начиная с C++17, в
auto s = f();
нет использования copy ctor. Объект будет построен непосредственно на складеs
. Вы даже можете=delete
скопировать вектор, и программа все равно скомпилируется и будет использовать вектор по умолчанию. Демо