Кто перехватывает исключения из конструктора копирования в операторе возврата (C++)?

Предположим, у меня есть функция, возвращающая значение.

S f(); 

и я называю это так:

auto s = f();

Если копия-конструктор S выбрасывает и return some_s; внутри f окружен блоком try-catch, гарантировано ли, что исключение будет перехвачено внутри f, а не кинут на место звонка?

Я понимаю, что auto s = f(); подлежит обязательному удалению копирования, начиная с C++17, поэтому существует (максимум) один вызов конструктора копирования S. Но гарантированно ли это произойдет внутри f с точки зрения обработки исключений?

Тесты в gcc, clang, mingw и msvc показывают, что это действительно так (код ниже). Меня беспокоит то, что

  • противоположно исключениям из конструкторов-копий параметров функций, которые обрабатываются на месте вызова
  • кажется странным, что return на самом деле не передает управление из функции

В любом случае некоторые цитаты из стандарта будут признательны ;)

Вот код, который я использовал для тестирования

#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;
}

Начиная с C++17, в auto s = f(); нет использования copy ctor. Объект будет построен непосредственно на складе s. Вы даже можете =delete скопировать вектор, и программа все равно скомпилируется и будет использовать вектор по умолчанию. Демо

user12002570 19.08.2024 11:33

@user12002570 user12002570 Спасибо, как сказано в вопросе, я это знаю. Но означает ли это, что на месте вызова не будет создано никаких исключений?

Adrian 19.08.2024 11:37

@user12002570 f() возвращает stat, который будет использовать конструктор копирования, поэтому копирующий вектор не может быть deleted.

Ted Lyngmo 19.08.2024 11:37

@TedLyngmo Да, return stat; здесь будет использоваться копировальный аппарат. Я имел в виду return S(7). Спецификацию проверю.

user12002570 19.08.2024 11:40

Интересный крайний случай: функциональные блоки try. Все тело функции может представлять собой блок try-catch.

MSalters 19.08.2024 14:34

@MSalters Да, это блокировка функции

Ted Lyngmo 19.08.2024 14:44
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
6
159
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Начиная с C++17, после возврата f копия отсутствует. Возвращаемое значение f — это нематериализованное временное значение, которое материализуется при инициализации s, оно не является объектом и его не нужно копировать для инициализации чего-либо.

Это не «обязательное удаление копии». Правила существования были изменены таким образом, что объекта (пока) нет.

кажется странным, что return на самом деле не передает управление из функции

В операторе return могут быть сколь угодно сложные подвыражения, которые могут выдавать ошибки. Если да, то вы можете перехватить их внутри функции. Было бы крайне неудобно, если бы вы не могли этого сделать.

Спасибо. Если g() вбрасывает return g();, мне ясно, что это происходит до фактического возврата. Здесь я спрашиваю, в какой именно момент в моем коде вызывается конструктор копирования (что касается обработки исключений).

Adrian 19.08.2024 11:45

@Адриан, помни, что stat — это выражение, и оно должно быть преобразовано из lvalue в rvalue, чтобы быть S вместо S&.

Caleth 19.08.2024 11:51

@ 463035818_is_not_an_ai В МОЕМ коде ЕСТЬ копия. Вы изменили мой return stat; на возвращаемое значение.

Adrian 19.08.2024 11:54
f()нет РВО. Он возвращает именованный статический объект и должен использовать конструктор копирования (или конструктор перемещения, если бы он существовал).
Ted Lyngmo 19.08.2024 12:06

@Калет, еще раз спасибо, но я все еще не уверен. Вы говорите: «f возвращает нематериализованное временное значение, которое материализуется при инициализации s». Кажется, это подразумевает, что инициализация (т. е. построение копирования) происходит ПОСЛЕ возврата функции, а не внутри try блока f.

Adrian 19.08.2024 12:10

@Адриан, после возвращения f копии не будет. Значение, возвращаемое f, не является объектом до тех пор, пока оно кому-то не понадобится, в данном случае это инициализация s. Переход C++14 -> C++17 приводит к серьезным изменениям в поведении абстрактной машины (которую описывает стандарт), даже если реализации остались в основном прежними.

Caleth 19.08.2024 12:12

@Калет Хорошо, значит, правило № 12 из en.cppreference.com/w/cpp/language/eval_order#Rules все еще применимо здесь, верно? Вы говорите, что конструктор копирования гарантированно вызывается «внутри» f? Из статьи об исключении копирования на cppreference это неясно.

Adrian 19.08.2024 12:27

@Адриан, тебе это не нужно. Граммер — это обработчик-seq составного оператора, который сопоставляет любое исключение, выданное составным оператором, с обработчиками в handler-seq. В try есть копия, это один из случаев инициализации копирования. Формулировка cppreference немного вводит в заблуждение, поскольку говорит об инициализации объектов, что не подходит для случая return stat.

Caleth 19.08.2024 12:39

@Caleth Но откуда вы знаете, что «есть копия в возвращаемой статистике»? Я считаю, что вы правы, но как это следует из стандарта? Я могу поверить, что правила C++ 17 гласят, что вызывается конструктор копирования один раз (без RVO, как в моем случае), поэтому моя проблема сводится к вопросу, как мы узнаем, что он вызывается в ответном stat.

Adrian 19.08.2024 15:34

@Адриан Кажется, ты отклонился от заданного вопроса. Я думаю, вам следует посмотреть, получили ли вы ответ на вопрос в каком-либо из полученных ответов, а затем, возможно, задать новый вопрос о том, когда NRVO разрешено, а когда RVO обязательно. Копирование Elision @ cppreference может быть хорошим началом. Вы обнаружите, что NRVO (в return stat;) не разрешен. Поскольку это запрещено, его необходимо скопировать.

Ted Lyngmo 19.08.2024 17:22

@Adrian По сути, это особое поведение prvalues. Начиная с C++17, они больше не являются «временными значениями», а являются «рецептами инициализации». По сути, последовательность «копий» prvalue исключается так, что пункт назначения на одном конце последовательности (s) инициализируется непосредственно конструкцией, которая «инициализировала» prvalue на другом конце последовательности (конструктор копирования в return stat). .

j6t 19.08.2024 17:30

Я считаю, что на этот вопрос можно ответить без языкового юриста.

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 делают это еще более запутанным (по крайней мере, для меня).

Adrian 19.08.2024 12:05

гарантированно ли это произойдет внутри f с точки зрения обработки исключений?

Да.

C++17:

18.1. Вызов исключения

  1. При возникновении исключения управление передается ближайшему обработчику соответствующего типа ([Exception.handle] ); «ближайший» означает обработчик, для которого составной оператор или ctor-инициализатор, следующий за ключевым словом try, был последним введен потоком управления и еще не завершился.

Когда конструктор копирования добавляет ваш пример, блок try в f() еще не закрыт. Исключение возникает до завершения вычисления полного выражения в операторе return.

Это означает, что управление передается ближайшему обработчику соответствующего типа, который также находится внутри f().

Спасибо! Что меня все еще беспокоит, так это то, как это взаимодействует с обязательным «копированием» операнда в операторе возврата С++ 17.

Adrian 19.08.2024 12:30

@Adrian В return stat; невозможно NRVO (называемое оптимизацией возвращаемого значения, ранее копированием), а тем более RVO (ранее называвшееся обязательным исключением копирования).

Ted Lyngmo 19.08.2024 12:33

Да, именно поэтому я сделал его статическим. Я должен подумать об этом. Спасибо.

Adrian 19.08.2024 12:35

@Адриан Адриан Всегда пожалуйста! Я надеюсь, что отрывок из стандарта в конечном итоге прояснит, что исключение гарантированно будет перехвачено функцией (если для него установлен обработчик, как в вашем случае). Если убрать сложность занятий, я думаю, за этим будет легче следить. пример

Ted Lyngmo 19.08.2024 12:46

Конструктор копирования, работающий в return stat;, инициализирует переменную s в auto s = f();. Несмотря на такой долгосрочный эффект вне функции, он по-прежнему выполняется внутри функции как часть оператора return. Следовательно, его исключения могут быть перехвачены функцией.

j6t 19.08.2024 13:16

Да, действительно, я забыл обратиться к @Adrian.

j6t 19.08.2024 13:22

@j6t Я думаю, ты прав. И вы обращаетесь именно к моей проблеме! Но я пытаюсь соединить точки: как именно это следует из правил языка? Откуда вы знаете, что «конструктор копирования запускается в return stat» ввиду правил C++17 о «исключении копирования»?

Adrian 19.08.2024 15:25

@Adrian В return stat; нет копирования. Каждое полное выражение упорядочивается перед следующим полным выражением, поэтому returnexpression; необходимо полностью вычислить перед передачей управления куда-либо. Раз бросает, то контроль пока в пределах f().

Ted Lyngmo 19.08.2024 15:28

@Адриан, мы знаем, потому что stat — это выражение lvalue, обозначающее объект в статическом хранилище, поэтому return stat соответствует инициализации копирования правила грамматики.

Caleth 19.08.2024 15:29

@Caleth Я не понимаю, как это подразумевает стандартный текст об инициализации копирования. Но я обнаружил, что примечание в timsong-cpp.github.io/cppwp/n4659/stmt.return#2 явно говорит: «Оператор return может включать вызов конструктора для выполнения копирования или перемещения». Вы знаете, где в стандарте описана вся эта «материализация». Текст в /n4659/class.temporary не особо информативен.

Adrian 19.08.2024 17:17

@Адриан, это происходит из определения категорий значений Basic.lval

Caleth 19.08.2024 17:32

@Адриан «Временная материализация» — это противоположность тому, что происходит здесь.

j6t 19.08.2024 17:34

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