Подтвердить что ты не робот

Когда конструктор копирования вызывает эту структуру?

Я пытаюсь выполнить некоторые тесты с помощью {} -листов. Когда я скомпилировал это в VS2015, вывод

copy A 0

Просто не получишь, где называется конструктор копирования?

#include <iostream>

struct A
{
    A() = default;
    A(int i) : m_i(i) {}
    A(const A& a)
    {
        std::cout << "copy A " << m_i << std::endl;
    }
    int m_i = 0;
};

struct B
{
    B(const A& a) : m_a(a) {}
    B(const A& a1, const A& a2) {}
    A m_a;
};

int main()
{
    A a1{1}, a2{2};
    B b({ a1, a2 });
    return 0;
}
4b9b3361

Ответ 1

Краткая версия:

В прямой инициализации, такой как B b({a1, a2}), бит-init-list {a1, a2} рассматривается как один аргумент для конструктора B. Этот аргумент {a1, a2} будет использоваться для инициализации первого параметра конструктора. B содержит неявно объявленный конструктор B(B const&). Ссылка B const& может быть инициализирована из {a1, a2} путем создания временного B. Этот временной элемент содержит подобъект A, и этот подобъект, наконец, будет скопирован в b.m_a с помощью конструктора копирования B(B const&).

Сравнить с:

void foo(B const& b0);
foo({a1, a2}); // one argument, creates a temporary `B`

Мы не увидим никакого копирования для инициализаций формы B b{a1, a2} и B b(a1, a2) и B b = {a1, a2}, поскольку эти случаи рассматривают a1 и a2 как (отдельные) аргументы - если только жизнеспособный std::initializer_list существует.


Длинная версия:

Класс B содержит следующие конструкторы:

B(const A& a) : m_a(a) {}                      // #0
B(const A& a1, const A& a2) {}                 // #1
B(B const&) = default; // implicitly declared     #2
B(B&&) = default;      // implicitly declared     #3

#3 не будет присутствовать в VS2013 из-за отсутствия поддержки неявно предоставленных специальных функций перемещения. # 0 не используется в программе OP.

Инициализация B b({a1, a2}) должна выбрать один из этих конструкторов. Мы приводим только один аргумент {a1, a2}, поэтому # 1 не является жизнеспособным. # 0 также не является жизнеспособным, так как A не может быть построено из двух аргументов. Оба # 2 и # 3 по-прежнему жизнеспособны (# 3 не существует в VS2013).

Теперь разрешение перегрузки пытается инициализировать B const& или B&& из {a1, a2}. Временная B будет создана и привязана к этой ссылке. Разрешение перегрузки будет предпочтительнее № 3 до # 2, если существует # 3.

Создание временного повторения снова рассмотрит четыре конструктора, показанных выше, но теперь у нас есть два аргумента a1 и a2 (или initializer_list, но это не имеет значения здесь). # 1 - единственная жизнеспособная перегрузка, а временная - через B(const A& a1, const A& a2).

Таким образом, мы по существу оказываемся в основном B b( B{a1, a2} ). Копировать (или переместить) из временного B{a1, a2} в B можно удалить (copy-elision). Вот почему g++ и clang++ не называют копией ctor, ни движением ctor ни B, ни A.

VS2013, похоже, не справляется с построением копирования здесь, и он не может двигаться-построить, поскольку он не может неявно предоставить # 3 (VS2015 исправит это). Поэтому VS2013 вызывает B(B const&), который копирует B{a1, a2}.m_a в b.m_a. Это вызывает конструктор A copy.

Если # 3 существует, и движение не отклонено, вызывается неявно объявленный конструктор перемещения # 3. Поскольку A имеет явно объявленный конструктор копирования, конструктор перемещения неявно объявляется для A. Это также приводит к построению копии от B{a1, a2}.m_a до b.m_a, но через перемещение ctor B.


В VS2013, если мы вручную добавим move ctor в A и B, заметим, что A будет перемещен вместо скопированного:

#include <iostream>
#include <utility>

struct A
{
    A() = default;
    A(int i) : m_i(i) {}
    A(const A& a)
    {
        std::cout << "copy A " << m_i << std::endl;
    }
    A(A&& a)
    {
        std::cout << "move A " << m_i << std::endl;
    }
    int m_i = 0;
};

struct B
{
    //B(const A& a) : m_a(a) {}
    B(const A& a1, const A& a2) {}
    B(B const&) = default;
    B(B&& b) : m_a(std::move(b.m_a)) {}
    A m_a;
};

Как правило, проще понять такие программы, отслеживая каждый конструктор. Использование специфичного для MSVC __FUNCSIG__ (g++/clang++ может использовать __PRETTY_FUNCTION__):

#include <iostream>

#define PRINT_FUNCSIG() { std::cout << __FUNCSIG__ << "\n"; }

struct A
{
    A() PRINT_FUNCSIG()
    A(int i) : m_i(i) PRINT_FUNCSIG()
    A(const A& a) : m_i(a.m_i) PRINT_FUNCSIG()
    int m_i = 0;
};

struct B
{
    B(const A& a1, const A& a2) PRINT_FUNCSIG()
    B(B const& b) : m_a(b.m_a) PRINT_FUNCSIG()
    A m_a;
};

int main()
{
    A a1{1}, a2{2};
    B b({ a1, a2 });
    return 0;
}

Отпечатывает (без комментариев):

__thiscall A::A(int)  // a1{1}
__thiscall A::A(int)  // a2{2}
__thiscall A::A(void) // B{a1, a2}.m_a, default-constructed
__thiscall B::B(const struct A &,const struct A &) // B{a1, a2}
__thiscall A::A(const struct A &) // b.m_a(B{a1, a2}.m_a)
__thiscall B::B(const struct B &) // b(B{a1, a2})

Дополнительные фактоиды:

  • Оба VS2015 и VS2013 do превышают конструкцию копирования B b(B{a1, a2});, но не оригинал B b({a1, a2}).