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

Это кажется глупым вопросом, но это точный момент, когда 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 является "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" ( см. в прямом эфире).

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

3 ответа

Решение

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

10.9.5 Скопировать / переместить метку (акценты мои):

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

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

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

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

Случай 1.

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

Случай 2

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

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

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

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

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

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

Из-за оптимизации возвращаемого значения (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 не применяется, и значение будет скопировано в возвращаемое значение точно при вызове return, и WriterДеструктор побежит на res.first после того, как копия произошла.

6.6 Прыжки

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

...

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

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

...

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

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

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

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

В C++ есть понятие, называемое elision.

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

До C++17 может произойти исключение:

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

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

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

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

RVO имеет место так:

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

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

До C++17 здесь должны существовать конструкторы копирования / перемещения, а исключение необязательно; в C++17 из-за новых правил prvalue не требуется никакого конструктора копирования / перемещения, и для компилятора не существует опций, здесь должно быть 1 значение.

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

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

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

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

Исключение - это правило, которое может возникнуть, даже если удаление этих копий, разрушений и конструкций будет иметь заметные побочные эффекты; это не оптимизация "как будто". Вместо этого это тонкое изменение по сравнению с тем, что наивный человек может думать, что означает код 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 вместе это не законно, и их жизни совпадают.

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

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