Ответ 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})
.