Является ли вызывающий или вызываемый объект копирующим или перемещающим возвращаемое значение функции? Например, если я хочу реализовать функцию pop () очереди, как это
template <typename T>
class queue
{
std::deque<T> d;
public:
// ... //
T pop()
{
// Creates a variable whose destructor removes the first
// element of the queue if no exception is thrown.
auto guard = ScopeSuccessGuard( [=]{ d.pop_front(); } );
return d.front();
}
}
вызывается деструктор моего охранника области после копирования переднего элемента?
РЕДАКТИРОВАТЬ: Дополнительный вопрос: будет ли линия
auto item = q.pop();
быть строго исключительным сейчас?
Возвращаемое значение копируется до выхода локальных переменных из области видимости. Копирование / перемещение может быть во временное местоположение (стек или регистр (ы)) или напрямую в собственный буфер вызывающего абонента или предпочтительные регистры — это проблема оптимизации / встраивания.
Когда используется временное расположение, компилятор должен организовать некоторое разделение работы между вызывающим и вызываемым объектами, и существует ряд соглашений для возвращаемых значений (и, конечно, параметров функций) для конкретных ОС и двоичных объектов / исполняемого формата, таких, что библиотеки / объекты, скомпилированные с одним компилятором, обычно могут использоваться с другим.
Будет ли линия …
auto item = q.pop();
…быть строго исключительным?
Если предположить, pop_front()
не может throw
интересным случаем является возвращение временного местоположения, из которого значение снова копируется в буфер вызывающей стороны после возврата функции. Мне кажется, что вы не защитили себя от этого. Elision (вызываемый объект, непосредственно создающий возвращаемое значение в буфере / регистре (ах) результата вызывающего абонента) разрешен, но не обязателен.
Чтобы изучить это, я написал следующий код:
#include <iostream>
struct X
{
X() { std::cout << "X::X(this " << (void*)this << ")\n"; }
X(const X& rhs) { std::cout << "X::X(const X&, " << (void*)&rhs
<< ", this " << (void*)this << ")\n"; }
~X() { std::cout << "X::~X(this " << (void*)this << ")\n"; }
X& operator=(const X& rhs)
{ std::cout << "X::operator=(const X& " << (void*)&rhs
<< ", this " << (void*)this << ")\n"; return *this; }
};
struct Y
{
Y() { std::cout << "Y::Y(this " << (void*)this << ")\n"; }
~Y() { std::cout << "Y::~Y(this " << (void*)this << ")\n"; }
};
X f()
{
Y y;
std::cout << "f() creating an X...\n";
X x;
std::cout << "f() return x...\n";
return x;
};
int main()
{
std::cout << "creating X in main...\n";
X x;
std::cout << "x = f(); main...\n";
x = f();
}
Компилирование с g++ -fno-elide-constructors
, мой вывод (с дополнительными комментариями) был:
creating X in main...
X::X(this 0x22cd50)
x = f(); main...
Y::Y(this 0x22cc90)
f() creating an X...
X::X(this 0x22cc80)
f() return x...
X::X(const X&, 0x22cc80, this 0x22cd40) // copy-construct temporary
X::~X(this 0x22cc80) // f-local x leaves scope
Y::~Y(this 0x22cc90)
X::operator=(const X& 0x22cd40, this 0x22cd50) // from temporary to main's x
X::~X(this 0x22cd40)
X::~X(this 0x22cd50)
Очевидно, что назначение произошло после f()
левая область действия: любое исключение из этого будет после того, как ваша защита области видимости (здесь обозначена Y) была уничтожена.
То же самое происходит, если основной содержит X x = f();
или же X x(f());
за исключением того, что это конструктор копирования, который вызывается после уничтожения f()
локальные переменные
(Я ценю, что поведение одного компилятора иногда является плохой основой для рассуждений о том, требуется ли что-либо для работы с помощью Стандарта, но с другой стороны, это значительно более надежно: когда он не работает, этот компилятор сломан — что относительно редко — или Стандарт не требует этого. Здесь поведение компилятора просто используется, чтобы добавить неподтвержденный вес моему впечатлению о требованиях Стандарта.)
Немного подробностей для любопытных: не то, чтобы обычно было полезно иметь код, который можно вызывать только одним способом, а то, что может быть быть в безопасности это const X& x = f();
как const
ссылка продлевает срок службы временного, но я не могу убедить себя, что стандарт требует иметь временную функцию, срок жизни которой был продлен, временную функцию, скопированную без дополнительной копии; для чего бы это ни стоило — это «сработало» в моей программе, и, что интересно, временное хранилище занимает то же место в стеке, которое используется при исключении возвращаемого значения, что предполагает f()
код эффективно скомпилирован с возможностью элиды и -f-no-elide-constructors
опция не столько отключает оптимизацию, сколько старается добавить пессимизацию: оставляя дополнительное место в стеке для временного объекта перед вызовом функции, затем добавляя дополнительный код для его копирования и уничтожая временный объект, затем перенастраивая указатель стека … ,
Копия возвращаемого значения выполняется вызываемой стороной и должна быть сделана до вызова деструкторов, так как в противном случае вы не могли бы вернуть значение / содержимое локальной переменной.
Вот соответствующий раздел в стандарте: раздел 12.4, пункт 11 (деструкторы)
Деструкторы вызываются неявно
- для построенных объектов с автоматической продолжительностью хранения (3.7.3) при выходе из блока, в котором создан объект (6.7)
Я пытался найти место, где говорилось, что «возвращение происходит до разрушения», но в нем не говорится так ясно, как хотелось бы [если я что-то не упустил].