Точный момент "возврата" в функции С++

Это похоже на глупый вопрос, но это точный момент, когда return xxx; "выполняется" в функции, однозначно определенной?

Пожалуйста, посмотрите следующий пример, чтобы увидеть, что я имею в виду (здесь живут):

#include <iostream>
#include <string>
#include <utility>

//changes the value of the underlying buffer
//when destructed
class Writer{
public:
    std::string &s;
    Writer(std::string &s_):s(s_){}
    ~Writer(){
        s+="B";
    }
};

std::string make_string_ok(){
    std::string res("A");
    Writer w(res);
    return res;
}


int main() {
    std::cout<<make_string_ok()<<std::endl;
} 

То, что я наивно ожидаю, когда make_string_ok вызывается:

  1. res конструктор для res (значение res равно "A")
  2. Конструктор для w называется
  3. выполняется return res. Текущее значение res должно быть возвращено (путем копирования текущего значения res), то есть "A".
  4. Вызывается деструктор для w, значение res становится "AB".
  5. Вызывается деструктор для res.

Поэтому я ожидал бы "A" в результате, но получим "AB" напечатанный на консоли.

С другой стороны, для немного другой версии make_string:

std::string make_string_fail(){
    std::pair<std::string, int> res{"A",0};
    Writer w(res.first);
    return res.first;
}

результат будет таким, как ожидалось, - "A" (см. live).

Указывает ли стандарт, какое значение должно быть возвращено в приведенных выше примерах или оно не указано?

Ответы

Ответ 1

Это RVO (+ возвращение копии как временное, которое туманное изображение), одна из оптимизаций, которым разрешено изменять видимое поведение:

10.9.5 Копирование/перемещение elision (акценты мои):

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

Это разрешение операций копирования/перемещения, называемое копированием, разрешено в следующих случаях (которые могут быть объединены для устранения нескольких копий):

  • в выражении return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или переменной, введенной объявлением исключения обработчиком) с тем же типом ( игнорируя cv-qualification) в качестве возвращаемого типа функции, операция копирования/перемещения может быть опущена путем создания автоматического объекта непосредственно в объект возврата вызова функции
  • [...]

Основываясь на том, применяет ли оно ваше помещение, вы ошибаетесь. В 1. вызывается c'tor для res, но объект может make_string_ok внутри make_string_ok или снаружи.

Случай 1.

Пули 2. и 3. могут вообще не произойти, но это боковая точка. Цель получила побочные эффекты от Writer dtor, была вне make_string_ok. Это было временно создано с помощью make_string_ok в контексте operator<<(ostream, std::string) оценки operator<<(ostream, std::string). Компилятор создал временное значение, а затем выполнил функцию. Это важно, потому что временная жизнь за его пределами, поэтому цель для Writer не локальна для make_string_ok а для operator<<.

Случай 2.

Между тем, ваш второй пример не соответствует критерию (и не опущен для краткости), потому что типы разные. Так писатель умирает. Он даже умрет, если бы он был частью pair. Итак, здесь копия res.first возвращается как временный объект, а затем dtor of Writer влияет на исходный res.first, который вот-вот умрет сам.

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

В конце концов, это сводится к RVO, потому что d'the Writer либо работает на внешнем объекте, либо на локальном, в зависимости от того, применяется ли оптимизация или нет.

Указывает ли стандарт, какое значение должно быть возвращено в приведенных выше примерах или оно не указано?

Нет, оптимизация не является обязательной, хотя она может изменить наблюдаемое поведение. Это по усмотрению компилятора применить это или нет. Он освобождается от правила "общего как-если", в котором говорится, что компилятор разрешен для любого преобразования, которое не меняет наблюдаемого поведения.

Случай для него стал обязательным в С++ 17, но не вашим. Обязательным является то, что возвращаемое значение является неназванным временным.

Ответ 2

Из-за оптимизации возвращаемого значения (RVO) деструктор для std::string res в make_string_ok не может быть вызван. string объект может быть построен на стороне вызывающего абонента, и функция может только инициализировать значение.

Код будет эквивалентен:

void make_string_ok(std::string& res){
    Writer w(res);
}

int main() {
    std::string res("A");
    make_string_ok(res);
}

Вот почему возврат значения должен быть "AB".

Во втором примере RVO не применяется, и значение будет скопировано в возвращаемое значение точно по вызову для возврата, а деструктор Writer будет выполняться на res.first после того, как произошла копия.

6.6.

При выходе из области действия (как бы это было сделано) деструкторы (12.4) вызываются для всех построенных объектов с автоматическим временем хранения (3.7.2) (именованные объекты или временные), объявленные в этой области, в обратном порядке их объявления. Передача из цикла, из блока или обратно после инициализированной переменной с автоматическим временем хранения включает в себя уничтожение переменных с автоматическим временем хранения, которые находятся в области в точке, переданной из...

...

6.6.3. Заявление о возврате

Инициализация копии возвращаемого объекта секвенируется до уничтожения временных рядов в конце полного выражения, установленного операндом оператора return, который, в свою очередь, упорядочен до уничтожения локальных переменных (6.6) блок, содержащий оператор return.

...

12.8 Копирование и перемещение объектов класса

31 Когда выполняются определенные критерии, реализации разрешается опустить конструкцию копирования/перемещения объекта класса, даже если конструктор copy/move и/или деструктор объекта имеют побочные эффекты. В таких случаях реализация рассматривает источник и цель пропущенной операции копирования/перемещения как просто два разных способа обращения к одному и тому же объекту, а уничтожение этого объекта происходит в более поздние времена, когда эти два объекта были бы (123) Это разрешение операций копирования/перемещения, называемое копированием, разрешено в следующих случаях (которые могут быть объединены для исключения нескольких копий):

- в операторе return в функции с типом возвращаемого класса, когда выражение является именем энергонезависимого автоматического объекта (кроме функции или параметра catch-clause) с тем же самым cvunqualified типом, что и возвращаемый тип функции, операция копирования/перемещения может быть опущена путем создания автоматического объекта непосредственно в возвращаемое значение функции

123) Поскольку только один объект уничтожается вместо двух, а один конструктор копирования/перемещения не выполняется, для каждого сконструированного объекта все еще уничтожается.

Ответ 3

В C++ есть концепция, называемая элизией.

Elision принимает два, казалось бы, отдельных объекта и объединяет их личность и время жизни.

До может возникнуть конфликт:

  1. Когда у вас есть непараметрическая переменная Foo f; в функции, которая вернула Foo а оператор return был простым return f; ,

  2. Когда у вас есть анонимный объект, который используется для создания почти любого другого объекта.

В все (почти?) Случаи № 2 устраняются новыми правилами prvalue; elision больше не возникает, потому что то, что используется для создания временного объекта, больше не делает этого. Вместо этого построение "временного" напрямую связано с местом постоянного объекта.

Теперь исключение не всегда возможно при условии, что ABI компилируется компилятором. Два распространенных случая, когда это возможно, известны как Оптимизация возвращаемого значения и Оптимизация с наименьшими возвращаемыми значениями.

RVO имеет следующий вид:

Foo func() {
  return Foo(7);
}
Foo foo = func();

где у нас есть возвращаемое значение Foo(7) которое возвращается в возвращаемое значение, которое затем возвращается во внешнюю переменную foo. То, что кажется 3 объектами (возвращаемое значение foo(), значение в return строке и Foo foo), фактически равно 1 во время выполнения.

До здесь должны существовать конструкторы copy/move, а elision - необязательные; в из-за новых правил prvalue нет необходимости в copy/move constructr, и для компилятора нет опции, здесь должно быть 1 значение.

Другой известный случай называется оптимизацией возвращаемого значения, NRVO. Это вышеописанный случай (1).

Foo func() {
  Foo local;
  return local;
}
Foo foo = func();

опять же, elision может объединить время жизни и идентичность Foo local, возвращаемое значение func и Foo foo вне func.

Даже , второе слияние (между возвращаемым значением func и Foo foo) не является необязательным (и технически значение prvalue, возвращаемое func, никогда не является объектом, а просто выражением, которое затем связано с построением Foo foo), но сначала остается необязательным, и требуется, чтобы существовал механизм перемещения или копирования.

Elision - это правило, которое может произойти, даже если исключение этих копий, разрушений и конструкций будет иметь наблюдаемые побочные эффекты; это не оптимизация "как-если". Вместо этого это тонкое изменение от того, что наивный человек может подумать о коде C++. Называть его "оптимизацией" является нечто большее, чем немного неправильное.

Факт, что он не является обязательным, и что тонкие вещи могут его нарушить, это проблема с ним.

Foo func(bool b) {
  Foo long_lived;
  long_lived.futz();
  if (b)
  {
    Foo short_lived;
    return short_lived;
  }
  return long_lived;
}

в приведенном выше случае, в то время как для компилятора законно исключать как Foo long_lived и Foo short_lived, проблемы с реализацией делают это в принципе невозможным, так как оба объекта не могут одновременно объединяться с возвращаемым значением func; eliding short_lived и long_lived вместе не являются законными, и их время жизни перекрывается.

Вы все еще можете сделать это с помощью as-if, но только если вы сможете изучить и понять все побочные эффекты деструкторов, конструкторов и .futz().