Как на самом деле функция возвращает значение?

Если у меня есть класс A (который возвращает объект по значению) и две функции f() и g(), имеющие разницу только в своих возвращаемых переменных:

class A
{
    public:
    A () { cout<<"constructor, "; }
    A (const A& ) { cout<<"copy-constructor, "; }
    A& operator = (const A& ) { cout<<"assignment, "; }
    ~A () { cout<<"destructor, "; }
};
    const A f(A x)
    {A y; cout<<"f, "; return y;}

    const A g(A x)
    {A y; cout<<"g, "; return x;}

main()
{
    A a;
    A b = f(a);
    A c = g(a);
}

Теперь, когда я выполняю строку A b = f(a);, она выводит:

copy-constructor, constructor, f, destructor, что хорошо, если предположить, что объект y в f() создается непосредственно в месте назначения, т. е. в ячейке памяти объекта b, без участия временных объектов.

А когда я выполняю строку A c = g(a);, она выводит:

copy-constructor, constructor, g, copy-constructor, destructor, destructor,.

Итак, вопрос в том, почему в случае g() объект не может быть создан непосредственно в ячейке памяти c, как это произошло при вызове f()? Почему он вызывает дополнительный конструктор-копию (который, я полагаю, из-за участия временного) во втором случае?


person cirronimbo    schedule 06.06.2012    source источник
comment
Если вы хотите, чтобы компилятор выполнял оптимизацию, вам придется компилировать с включенной оптимизацией.   -  person Hans Passant    schedule 06.06.2012
comment
Я не думаю, что это имеет какое-либо отношение к оптимизации компилятора, поскольку я уже пробовал это.   -  person cirronimbo    schedule 06.06.2012


Ответы (4)


Разница в том, что в случае g вы возвращаете значение, которое было передано функции. В стандарте явно указано, при каких условиях копия может быть удалена в 12.8p31, и он не включает исключение копии из аргумента функции.

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

Некоторое время назад я завел недолговечный блог (ожидал, что у меня будет больше времени...) и написал пару статей о NRVO и копировании, которые могут помочь прояснить это (или нет, кто знает :)):

Семантика значений: NRVO

Семантика значений: исключение копирования

person David Rodríguez - dribeas    schedule 06.06.2012
comment
Большое спасибо. Ваше [не]определенное поведение разрешило многие мои сомнения, безусловно, четко определенным образом :) Но у меня есть еще несколько сомнений для вас, если вы можете сказать им: 1. В NRVO, когда вы использовали bool, чтобы выбрать из типа x и y, какой тип возвращать, кажется, что мой компилятор не может выполнить исключение (я сомневаюсь и в других). - person cirronimbo; 06.06.2012
comment
2. В моем коде, если я привязываю ссылку к временной, т.е. A & b = f(a); тогда происходит то, что область объекта (предположительно временная, возвращаемая f(a)) увеличивается до конечной скобки main. Итак, это противоречит двум вещам: 1. Как вы упомянули в своем блоге, использование временного адреса является незаконным, но мы делаем это здесь. 2. как временное может длиться так долго? - person cirronimbo; 06.06.2012
comment
3. Различаются ли области видимости локальных объектов-членов функции и ее аргументов. Когда я сделал что-то вроде A a; А б; б = г(а); затем в строке b = g(a) деструктор локального объекта вызывался до оператора присваивания, а деструктор аргумента — после присваивания. - person cirronimbo; 06.06.2012
comment
извините, из-за нехватки места cudnt напишите все три сомнения в одном (надеюсь, вы справитесь :) ) - person cirronimbo; 06.06.2012
comment
1. Это зависит от компилятора. Я бы не ожидал, что многие компиляторы подхватят его, но теоретически это возможно (вы пробовали с самым высоким уровнем оптимизации?) 2. В стандарте есть явное правило, позволяющее включить продление жизни при привязке константной ссылки, и вы не берете адрес, а только создаете ссылку (думайте о ссылке как о псевдониме), в этом конкретном случае компилятор может удалить ссылку из двоичного файла и просто замените использование ссылки использованием исходного объекта.... - person David Rodríguez - dribeas; 06.06.2012
comment
... временное значение длится дольше, чем выражение, за счет создания скрытой переменной в стеке (_Tmp в статье) и обработки ее как локальной переменной. 3. Я не очень понимаю, в чем заключается ваш вопрос. После исключения некоторых копий у вас есть несколько объектов: a, b, g_in, g_out, где g_in — аргумент, а g_out — возвращаемый объект. g_in уничтожается сразу после создания g_out и выхода из функции, до того, как g_out будет присвоено b; g_out должно уничтожаться в конце полного выражения, затем уничтожается b, затем a. - person David Rodríguez - dribeas; 06.06.2012

Проблема в том, что во втором случае вы возвращаете один из параметров. Учитывая, что обычно копирование параметра происходит на месте вызывающей стороны, а не внутри функции (в данном случае main), компилятор делает копию, а затем вынужден копировать ее снова, как только он входит в g().

Из http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

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

person Dave S    schedule 06.06.2012
comment
Мне еще предстоит найти компилятор, который пропускает копию при возврате параметра функции -- Никаких сюрпризов, невозможно иметь соглашение о вызовах, которое допускает это, и стандарт (приемлемо после того, как эта статья была написана) прямо говорится, что это не может быть сделано компилятором. - person David Rodríguez - dribeas; 06.06.2012

Вот небольшая модификация вашего кода, которая поможет вам прекрасно понять, что там происходит:

class A{
public:
    A(const char* cname) : name(cname){
        std::cout << "constructing " << cname << std::endl;
    }
    ~A(){
        std::cout << "destructing " << name.c_str() << std::endl;
    }
    A(A const& a){
        if (name.empty()) name = "*tmp copy*";
        std::cout 
            << "creating " << name.c_str() 
            << " by copying " << a.name.c_str() << std::endl;
    }
    A& operator=(A const& a){
        std::cout
            << "assignment ( "
                << name.c_str() << " = " << a.name.c_str()
            << " )"<< std::endl;
        return *this;
    }
    std::string name;
};

Вот использование этого класса:

const A f(A x){
    std::cout 
        << "// renaming " << x.name.c_str() 
        << " to x in f()" << std::endl;
    x.name = "x in f()";
    A y("y in f()");
    return y;
}

const A g(A x){
    std::cout 
        << "// renaming " << x.name.c_str()
        << " to x in f()" << std::endl;
    x.name = "x in g()";
    A y("y in g()");
    return x;
}

int main(){
    A a("a in main()");
    std::cout << "- - - - - - calling f:" << std::endl;
    A b = f(a);
    b.name = "b in main()";
    std::cout << "- - - - - - calling g:" << std::endl;
    A c = g(a);
    c.name = "c in main()";
    std::cout << ">>> leaving the scope:" << std::endl;
    return 0;
}

и вот вывод при компиляции без какой-либо оптимизации:

constructing a in main()
- - - - - - calling f:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in f()
creating *tmp copy* by copying y in f()
destructing y in f()
destructing x in f()
- - - - - - calling g:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in g()
creating *tmp copy* by copying x in g()
destructing y in g()
destructing x in g()
>>> leaving the scope:
destructing c in main()
destructing b in main()
destructing a in main()

Опубликованный вами результат — это результат работы программы, скомпилированной с помощью оптимизации именованного возвращаемого значения. В этом случае компилятор пытается устранить избыточные вызовы конструктора копирования и деструктора, что означает, что при возврате объекта он попытается вернуть объект, не создавая его избыточную копию. Вот вывод с включенным NRVO:

constructing a in main()
- - - - - - calling f:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in f()
destructing x in f()
- - - - - - calling g:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in g()
creating *tmp copy* by copying x in g()
destructing y in g()
destructing x in g()
>>> leaving the scope:
destructing c in main()
destructing b in main()
destructing a in main()

В первом случае *tmp copy* путем копирования y in f() не создается, так как NRVO выполнил свою работу. Однако во втором случае NRVO не может быть применен, потому что в этой функции был объявлен другой кандидат на слот возврата. Для получения дополнительной информации см.: C++: избежание копирования с помощью оператора return :)

person LihO    schedule 06.06.2012
comment
Да, я знаю это, и я также сделал это в своем коде, чтобы увидеть, что именно происходит (хотя я опубликовал упрощенную версию кода, подчеркнув только то, в чем заключалась моя проблема). И этот код не отвечает на вопрос, который я задаю. Меня спросили о ПРИЧИНЕ происходящего, а не о самом происшествии. В любом случае, спасибо за беспокойство :) - person cirronimbo; 06.06.2012
comment
@cirronimbo: Проверьте мой ответ сейчас, он объясняет, что происходит с включенным NRVO, а также объясняет, почему я предложил вам этот вопрос. - person LihO; 06.06.2012

он может (почти) оптимизировать весь вызов функции g(), и в этом случае ваш код будет выглядеть так:

A a;
A c = a;

как эффективно это то, что делает ваш код. Теперь, когда вы передаете a как параметр по значению (т.е. не ссылку), то компилятор почти должен выполнить копию там, а затем он возвращает этот параметр по значению, он должен выполнить еще одну копию.

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

person gbjbaanb    schedule 06.06.2012