Точный момент «возврата» в C ++ - функцию - PullRequest
0 голосов
/ 22 октября 2018

Это кажется глупым вопросом, но является ли точный момент, когда 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 is "A")
  2. Конструктор для w называется
  3. return res выполняется.Текущее значение res должно быть возвращено (путем копирования текущего значения res), т.е. вызывается "A".
  4. Деструктор для w, значение res становится "AB".
  5. Вызывается деструктор для res.

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

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

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

результат, как и ожидалось - "A" ( см. в прямом эфире ).

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

Ответы [ 3 ]

0 голосов
/ 22 октября 2018

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

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

До elisionможет произойти:

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

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

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

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

RVO имеет такой случай:

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

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

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

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

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

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

Даже , второе слияние (между возвращаемым значением func и Foo foo) не является обязательным (и технически значение, возвращаемое из 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;одновременное исключение short_lived и long_lived недопустимо, и время их жизни перекрывается.

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

0 голосов
/ 22 октября 2018

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

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

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

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

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

На основеот того, применяется ли это, вся ваша предпосылка становится неверной.В 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 возвращается как временный объект, а затем dtor из Writer влияет на оригинал res.first, который вот-вот умрет сам.

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

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

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

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

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

0 голосов
/ 22 октября 2018

Из-за Оптимизация возвращаемого значения (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 Оператор возврата

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

...

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

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

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

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

...