Почему конструктор копирования вызывается дважды в этом фрагменте кода?

Я играю с несколькими вещами, чтобы понять, как работают конструкторы копирования. Но я не могу понять, почему конструктор копирования вызывается дважды для создания x2. Я бы предположил, что он будет вызван один раз, когда возвращаемое значение createX() копируется в x2.
Я также рассмотрел несколько связанных вопросов по SO, но, насколько я могу судить, я не смог найти тот же простой сценарий, который я задаю здесь.

Кстати, я -fno-elide-constructors с -fno-elide-constructors, чтобы увидеть, что происходит без оптимизации.

#include <iostream>

struct X {
    int i{2};

    X() {
        std::cout << "default constructor called" << std::endl;
    }

    X(const X& other) {
        std::cout << "copy constructor called" << std::endl;
    }
};

X createX() {
    X x;
    std::cout << "created x on the stack" << std::endl;
    return x;
}

int main() {
    X x1;
    std::cout << "created x1" << std::endl;
    std::cout << "x1: " << x1.i << std::endl << std::endl;    

    X x2 = createX();
    std::cout << "created x2" << std::endl;
    std::cout << "x2: " << x2.i << std::endl;    

    return 0;
}

Это вывод:

default constructor called
created x1
x1: 2

default constructor called
created x on the stack
copy constructor called
copy constructor called
created x2
x2: 2

Может ли кто-нибудь помочь мне, что я пропускаю или пропускаю здесь?

Ответ 1

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

return x;

Вы копируете инициализируемый объект возвращаемого значения с x. Это первый вызов конструктора копирования, который вы видите. затем

X x2 = createX();

использует возвращенный объект для копирования инициализации x2 так что это вторая копия, которую вы видите.


Стоит отметить, что

return x;

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

Ответ 2

Первый экземпляр взамен createX

X createX() {
    X x;
    std::cout << "created x on the stack" << std::endl;
    return x; // First copy
}

Второй - создать x2 из временного возврата createX.

X x2 = createX(); // Second copy

Обратите внимание, что в С++ 17 вторая копия вынуждена быть исключенной.