инициализация списка копирования по сравнению с инициализацией прямого списка временных

Учитывая следующую структуру:

struct ABC
{
    ABC(){cout << "ABC" << endl;}
    ~ABC() noexcept {cout << "~ABC" << endl;}
    ABC(ABC const&) {cout << "copy" << endl;}
    ABC(ABC&&) noexcept {cout << "move" << endl;}
    ABC& operator=(ABC const&){cout << "copy=" << endl;}
    ABC& operator=(ABC&&) noexcept {cout << "move=" << endl;}
};

Результат:

std::pair<std::string, ABC> myPair{{}, {}};

is:

ABC
copy
~ABC
~ABC

В то время как вывод:

std::pair<std::string, ABC> myPair{{}, ABC{}};

is:

ABC
move
~ABC
~ABC

Пытаясь понять разницу между ними, я думаю, что определил, что в первом случае используется инициализация списка копирования, а во втором — инициализация прямого списка безымянного временного объекта (номера 7 и 2 соответственно здесь). : http://en.cppreference.com/w/cpp/language/list_initialization).

В поисках похожих вопросов я нашел это: Почему стандарт различает инициализацию прямого списка и инициализацию списка копирования? и это: Вызывает ли инициализация списка копирования концептуально вызов ctor копирования?.

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

Итак, вопрос: почему временное копируется в первом случае, но перемещается во втором? Что мешает его переместить в случае copy-list-initialization?

В качестве примечания следующий код:

std::pair<std::string, ABC> myPair = std::make_pair<string, ABC>({}, {});

Также приводит к вызову конструктора перемещения ABC (а не к вызову конструктора копирования), но могут быть задействованы другие механизмы.

Вы можете попробовать код (используя gcc-4.9.2 в режиме C++14) по адресу: https://ideone.com/Kc8xIn


person Hugo    schedule 05.05.2015    source источник


Ответы (1)


Как правило, списки инициализации в фигурных скобках, такие как {}, не являются выражениями и не имеют типа. Если у вас есть шаблон функции

template<typename T> void f(T);

и вызовите f( {} ), для T тип не будет выведен, и вывод типа завершится ошибкой.

С другой стороны, ABC{} является выражением prvalue типа ABC ("явное преобразование типа в функциональной нотации"). Для такого вызова, как f( ABC{} ), шаблон функции может вывести тип ABC из этого выражения.


В C++14, как и в C++11, std::pair имеет следующие конструкторы [pairs.pair]; T1 и T2 — это имена параметра шаблона шаблона класса std::pair:

pair(const pair&) = default;
pair(pair&&) = default;
constexpr pair();
constexpr pair(const T1& x, const T2& y);
template<class U, class V> constexpr pair(U&& x, V&& y);
template<class U, class V> constexpr pair(const pair<U, V>& p);
template<class U, class V> constexpr pair(pair<U, V>&& p);
template <class... Args1, class... Args2>
pair(piecewise_construct_t, tuple<Args1...>, tuple<Args2...>);

Обратите внимание, что есть конструктор

constexpr pair(const T1& x, const T2& y); // (C)

Но нет

constexpr pair(T1&& x, T2&& y);

вместо этого есть совершенно переадресация

template<class U, class V> constexpr pair(U&& x, V&& y); // (P)

Если вы попытаетесь инициализировать std::pair двумя инициализаторами, где хотя бы один из них является списком инициализации в фигурных скобках, конструктор (P) нежизнеспособен, так как он не может вывести аргументы своего шаблона.

(C) не является шаблоном конструктора. Типы его параметров T1 const& и T2 const& фиксируются параметрами шаблона класса. Ссылка на константный тип может быть инициализирована из пустого списка скобок-инициализации. Это создает временный объект, привязанный к ссылке. Поскольку указанный тип является const, конструктор (C) скопирует свои аргументы в элементы данных класса.


Когда вы инициализируете пару через std::pair<T,U>{ T{}, U{} }, T{} и U{} являются prvalue-выражениями. Шаблон конструктора (P) может вывести их типы и является жизнеспособным. Создание экземпляра, созданное после вывода типа, лучше подходит, чем конструктор (C), потому что (P) создаст параметры ссылки на rvalue и привяжет к ним аргументы prvalue. (C), с другой стороны, связывает аргументы prvalue со ссылками на lvalue.


Почему тогда живой пример перемещает второй аргумент при вызове через std::pair<T,U>{ {}, U{} }?

libstdc++ определяет дополнительные конструкторы. Ниже приведена выдержка из его реализации std::pair из 78536ab78e, без определений функций, некоторых комментариев и SFINAE. _T1 и _T2 — имена параметров шаблона шаблона класса std::pair.

  _GLIBCXX_CONSTEXPR pair();

  _GLIBCXX_CONSTEXPR pair(const _T1& __a, const _T2& __b); // (C)

  template<class _U1, class _U2>
constexpr pair(const pair<_U1, _U2>& __p);

  constexpr pair(const pair&) = default;
  constexpr pair(pair&&) = default;

  // DR 811.
  template<class _U1>
constexpr pair(_U1&& __x, const _T2& __y); // (X)

  template<class _U2>
constexpr pair(const _T1& __x, _U2&& __y); // (E) <=====================

  template<class _U1, class _U2>
constexpr pair(_U1&& __x, _U2&& __y);      // (P)

  template<class _U1, class _U2>
constexpr pair(pair<_U1, _U2>&& __p);

  template<typename... _Args1, typename... _Args2>
    pair(piecewise_construct_t, tuple<_Args1...>, tuple<_Args2...>);

Обратите внимание на шаблон конструктора (E): он скопирует первый аргумент и точно перенаправит второй. Для инициализации, такой как std::pair<T,U>{ {}, U{} }, это жизнеспособно, потому что нужно только вывести тип из второго аргумента. Это также лучшее совпадение, чем (C) для второго аргумента, и, следовательно, лучшее совпадение в целом.

Комментарий "DR 811" есть в исходниках libstdc++. Он ссылается на LWG DR 811, который добавляет некоторые SFINAE, но не новые конструкторы.

Конструкторы (E) и (X) являются расширением libstdc++. Однако я не уверен, что это соответствует требованиям.

libc++, с другой стороны, не имеет этих дополнительных конструкторов. Для примера std::pair<T,U>{ {}, U{} } будет скопирован второй аргумент.

Живая демонстрация с обеими реализациями библиотеки

person dyp    schedule 05.05.2015
comment
как можно выбрать конструктор с идеальной переадресацией для вызова myPair{{}, ABC{}};, если он не может вывести std::string из {}? - person Marc Andreson; 05.05.2015
comment
@MarcAndreson Ой, мой плохой. Я неправильно это понял. Если вы используете libstdС++, это может быть расширением. Позвольте мне попытаться найти его. - person dyp; 05.05.2015
comment
Причиной этих перегрузок в основном является поддержка таких вещей, как std::pair<std::unique_ptr<int>, int *> p(std::unique_ptr<int>(), 0);. См. PR 40925, - person T.C.; 05.05.2015
comment
@Т.С. PR 40925, кажется, содержит пример, похожий на LWG 811, но в конце вашего комментария есть запятая. Чего-то не хватает? - person dyp; 05.05.2015
comment
@dyp Я ссылался на комментарий 8, который представляет собой фиксацию, добавляющую эти перегрузки с комментарием Добавить, чтобы правильно обрабатывать типы только для перемещения при наличии нулевых указателей. - person T.C.; 06.05.2015
comment
@Т.С. А, спасибо. Мне не пришло в голову заглянуть в сообщение фиксации/изменения этого комментария. Я добавил его и пример в свое электронное письмо в std-discussion. - person dyp; 06.05.2015