Кто копирует возвращаемое значение функции?

Является ли вызывающий или вызываемый объект копирующим или перемещающим возвращаемое значение функции? Например, если я хочу реализовать функцию 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();

быть строго исключительным сейчас?

2 ответа

Решение

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

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

Будет ли линия...

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)

Я пытался найти место, где говорилось, что "возвращение происходит до уничтожения", но в нем не говорится так ясно, как мне бы хотелось [если я что-то упустил].

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