Почему мне нужно использовать кусочек_конструкции в map:: emplace для одиночных конструкторов arg объектов, не подлежащих копированию?

Следующий код не будет компилироваться в gcc 4.8.2. Проблема в том, что этот код попытается скопировать конструкцию std::pair<int, A>, которая не может произойти из-за struct A отсутствующих копий и перемещения конструкторов.

Является ли gcc неудачным здесь или я что-то не хватает?

#include <map>
struct A
{
  int bla;
  A(int blub):bla(blub){}
  A(A&&) = delete;
  A(const A&) = delete;
  A& operator=(A&&) = delete;
  A& operator=(const A&) = delete;
};
int main()
{
  std::map<int, A> map;
  map.emplace(1, 2); // doesn't work
  map.emplace(std::piecewise_construct,
          std::forward_as_tuple(1),
          std::forward_as_tuple(2)
  ); // works like a charm
  return 0;
}

Ответы

Ответ 1

Насколько я могу судить, проблема не вызвана map::emplace, а конструкторами pair:

#include <map>

struct A
{
    A(int) {}
    A(A&&) = delete;
    A(A const&) = delete;
};

int main()
{
    std::pair<int, A> x(1, 4); // error
}

Этот пример кода не компилируется ни с помощью coliru g++ 4.8.1, ни с clang++ 3.5, которые оба используют libstdС++, насколько я могу судить.

Проблема связана с тем, что, хотя мы можем построить

A t(4);

то есть std::is_constructible<A, int>::value == true, мы не можем неявно преобразовать int в A [conv]/3

Выражение e может быть неявно преобразовано в тип T тогда и только тогда, когда декларация T t=e; хорошо сформирована, для некоторой изобретенной временной переменной T.

Обратите внимание на инициализацию копирования (=). Это создает временную A и инициализирует T из этого временного файла [dcl.init]/17. Эта инициализация из временного пытается вызвать удаленное перемещение ctor A, что делает преобразование плохо сформированным.


Поскольку мы не можем преобразовать из int в A, конструктор pair, который можно было бы ожидать, отклоняется SFINAE. Такое поведение удивительно, N4387 - Улучшение пары и кортежа анализирует и пытается улучшить ситуацию, сделав вместо этого конструктор explicit, N4387 был проголосован на С++ 1z на собрании Lenexa.

Ниже описываются правила С++ 11.

Конструктор, который я ожидал вызвать, описан в [pairs.pair]/7-9

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

7   Требуется: is_constructible<first_type, U&&>::value - true и is_constructible<second_type, V&&>::value true.

8   Эффекты: конструктор сначала инициализирует std::forward<U>(x), а второй - std::forward<V>(y).

9   Замечания: если U неявно конвертируется в first_type или V неявно конвертируется в second_type this конструктор не участвует в разрешении перегрузки.

Обратите внимание на разницу между is_constructible в разделе Requires и "неявно конвертируется" в разделе "Примечания". Требования выполняются для вызова этого конструктора, но он может не участвовать в разрешении перегрузки (= должен быть отклонен через SFINAE).

Следовательно, для разрешения перегрузки необходимо выбрать "худшее совпадение", а именно: второй параметр - A const&. Временное создается из аргумента int и привязывается к этой ссылке, а эта ссылка используется для инициализации элемента данных pair (.second). Инициализация пытается вызвать удаленную копию ctor A, а конструкция пары плохо сформирована.


libstdС++ имеет (как расширение) некоторые нестандартные ctors. В последнем doxygen (и в 4.8.2) конструктор pair, который я ожидал назвать (будучи удивлен правила, требуемые Стандартом):

template<class _U1, class _U2,
         class = typename enable_if<__and_<is_convertible<_U1, _T1>,
                                           is_convertible<_U2, _T2>
                                          >::value
                                   >::type>
constexpr pair(_U1&& __x, _U2&& __y)
: first(std::forward<_U1>(__x)), second(std::forward<_U2>(__y)) { }

и тот, который на самом деле называется, является нестандартным:

// DR 811.
template<class _U1,
         class = typename enable_if<is_convertible<_U1, _T1>::value>::type>
constexpr pair(_U1&& __x, const _T2& __y)
: first(std::forward<_U1>(__x)), second(__y) { }

Программа плохо сформирована в соответствии со стандартом, она не просто отвергается этим нестандартным ctor.


Как заключительное замечание, здесь спецификация is_constructible и is_convertible.

is_constructible [meta.rel]/4

Учитывая следующий прототип функции:

template <class T>
typename add_rvalue_reference<T>::type create();

условие предиката для специализированной специализации is_constructible<T, Args...> должно выполняться тогда и только тогда, когда следующее определение переменной будет хорошо сформировано для некоторой изобретенной переменной T:

T t(create<Args>()...);

[Примечание. Эти токены никогда не интерпретируются как объявление функции. - end note] Проверка доступа выполняется как в контексте, не связанным с T и любым из Args. Учитывается только действительность непосредственного контекста инициализации переменных.

is_convertible [meta.unary.prop]/6:

Учитывая следующий прототип функции:

template <class T>
typename add_rvalue_reference<T>::type create();

условие предиката для типовой специализации is_convertible<From, To> должно выполняться, если и только если выражение возврата в следующем коде будет хорошо сформировано, включая любые неявные преобразования к возвращаемому типу функции:

To test() {
  return create<From>();
}

[Примечание. Это требование дает хорошо определенные результаты для ссылочных типов, типов пустот, типов массивов и типов функций. - end note] Проверка доступа выполняется так, как если бы в контексте, не связанном с To и From. Только справедливость непосредственного контекста выражения return-statement (включая преобразования в тип возврата).


Для вашего типа A,

A t(create<int>());

хорошо сформирован; Однако

A test() {
  return create<int>();
}

создает временный тип A и пытается переместить его в возвращаемое значение (копирование-инициализация). Это выбирает удаленный ctor A(A&&) и поэтому плохо сформирован.