Время жизни аргумента временного объекта в функции

Я прочитал несколько сообщений о временном времени жизни объекта. И одним словом, я узнаю, что:

временное уничтожается после окончания полного выражения, содержащего его.

Но этот код не в моих ожиданиях:

#include <memory> 
#include <iostream> 

void fun(std::shared_ptr<int> sp)
{
    std::cout << "fun: sp.use_count() == " << sp.use_count() << '\n'; 
    //I expect to get 2 not 1
} 

int main()
{
    fun(std::make_shared<int>(5));  

}

Поэтому я думаю, что у меня есть 2 объекта интеллектуальных указателей, один из них - std::make_shared<int>(5), временный неназванный объект и другой sp который является локальной переменной внутри функции. Поэтому, основываясь на моем понимании, временный не "умрет" до завершения вызова функции. Я ожидаю, что результат будет 2, а не 1. Что здесь не так?

Ответ 1

Pre-С++ 17, sp перемещается из временного, если переход не начинается с начала. В любом случае sp является единственным владельцем ресурса, поэтому счетчик использования справедливо сообщается как 1. Это перегрузка 10) в этой ссылке.

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

Начиная с С++ 17, временное создание не создается благодаря гарантированному копированию/перемещению elision, а sp создается на месте.


Точная формулировка из указанной ссылки:

10) Move-constructs shared_ptr из r. После построения *this содержит копию предыдущего состояния r, r пусто и его сохраненный указатель равен null. [...]

В нашем случае r относится к временному и *this to sp.

Ответ 2

имеет странную концепцию, известную как элиция.

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

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

A a = A{}; // A{} is elided with a
void f(A);
f(A{}); // temporary A{} is elided with argument of f
A g();
f(g()); // return value of g is elided with argument of f

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

A g() {
  A a;
  return a; // a is elided with return value of g
}
A f() {
  A x = g(); // x is elided with return value of g
             // which is elided with a within g
  return x;  // and is then elided with return value of f
}
A bob = f(); // and then elided with bob.

В приведенном выше коде существует только один экземпляр A; у него просто много имен.

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

После некоторые вещи, которые раньше были элизией, были (в некотором смысле) "гарантированным elision", что совсем другое. "Гарантированное исключение" - это в основном идея о том, что prvalues (вещи, которые раньше были временными в pre- ), теперь являются абстрактными инструкциями о том, как создать объект.

В определенных обстоятельствах временные объекты создаются из них, но в других они просто используются для создания какого-либо другого объекта в каком-либо другом месте.

Поэтому в вы должны подумать об этой функции:

A f();

как функция, которая возвращает инструкции о том, как создать A Когда вы это сделаете:

A a  = f();

вы говорите "используйте инструкции, которые f возвращает, чтобы построить A именем a ".

Аналогично, A{} уже не является временным, но не инструктирует, как создать A Если вы поместите его в строку самостоятельно, эти инструкции используются для создания временного, но в большинстве контекстов не существует логически или фактически существует временно.

template<class T, class...Us>
std::shared_ptr<T> make_shared(Us&&...);

это функция, которая возвращает инструкции о том, как создать shared_ptr<T>.

 fun(std::make_shared<int>(5)); 

здесь вы применяете эти инструкции к приложению fun, которое имеет тип std::shared_ptr<int>.

В pre- [С++ 17] без флагов несовместимого компилятора результат с elision практически такой же. В этом случае временная идентичность сливается с аргументом fun.

В практическом случае не будет временного shared_ptr со ссылочным числом 0; другие ответы, которые утверждают, что это неправильно. Единственный способ, которым это может произойти, - это передать флаги, которые ваш компилятор выполнил при помощи elision (вышеупомянутые флагов несовместимого компилятора).

Если вы проходите в таких флагах, shared_ptr перемещается из аргумента fun и существует с use_count ссылки 0. Так что use_count останется 0.

Ответ 3

Помимо перемещения std::shared_ptr, существует еще один аспект: создание на месте аргумента функции, переданного по значению. Это оптимизация, которую обычно выполняют компиляторы. Рассмотрим примерный тип

struct A {
   A() { std::cout << "ctor\n"; }
   A(const A&) { std::cout << "copy ctor\n"; }
};

вместе с функцией, которая принимает экземпляр A по значению

void f(A) {}

Когда параметр функции передается как r-значение, подобное этому

f(A{});

конструктор копирования не будет вызываться, если вы явно не компилируете с -fno-elide-constructors. В С++ 17 вы даже можете удалить конструктор копирования

A(const A&) = delete;

потому что копия элиции гарантирована. Имея это в виду: временный объект, который вы передаете в качестве аргумента функции, "уничтожается после окончания полного выражения, содержащего его", только если существует временное, а фрагмент кода может предполагать существование одного, даже если он легко (и поскольку С++ 17: гарантировано) оптимизирован.