Std:: pair: слишком строгий конструктор?
Я наткнулся на удивительное поведение нового конструктора std::pair
, который был введен с С++ 11. Я заметил проблему при использовании std::pair<int, std::atomic<int>>
, и это происходит, потому что std::atomic
не является ни возможностью копирования, ни перемещением. В следующем коде я заменяю std::atomic<int>
на foobar
для упрощения.
Следующий код компилируется отлично, как с GCC-4.9, так и с Clang-3.5 (с libС++ и без него):
struct foobar
{
foobar(int) { } // implicit conversion
// foobar(const foobar&) = delete;
};
std::pair<int, foobar> p{1, 2};
Ожидается такое поведение. Однако, когда я удаляю конструктор копирования foobar
, компиляция не выполняется. Он работает с кусочной конструкцией, но я думаю, что это не обязательно, из-за неявного преобразования от int
до foobar
. Я имею в виду конструктор со следующей сигнатурой:
template <typename U, typename V>
pair(U&& u, V&& v);
Можете ли вы объяснить, почему конструктор пары является настолько ограничительным и не допускает неявных преобразований для noncopyable/nonmovable типов?
Ответы
Ответ 1
Это дефект в стандарте (сначала я его не нашел, так как он сформулирован для tuple
).
https://wg21.link/lwg2051
Дальнейшее обсуждение и предложенная резолюция (голосование за С++ 1z в Lenexa в мае 2015 года):
https://wg21.link/n4387
Основная проблема заключается в том, что конвертирующие конструкторы pair
и tuple
проверяют is_convertible
который требует доступного конструктора копирования/перемещения.
En подробно: Шаблоны конструктора преобразования std::pair<T1, T2>
и std::tuple
выглядят так:
template<class U, class V>
constexpr pair(U&&, V&&);
Но это слишком жадно: при попытке использовать его с несовместимыми типами std::is_constructible<pair<T1, T2>, U, V>::value
, и std::is_constructible<pair<T1, T2>, U, V>::value
всегда будет true
потому что объявление этого Шаблон конструктора может быть создан для любых типов U
и V
Следовательно, нам нужно ограничить этот шаблон конструктора:
template<class U, class V,
enable_if_t<check_that_we_can_construct_from<U, V>::value>
>
constexpr pair(U&& u, V&& v)
: t1( forward<U>(u) ), t2( forward<V>(v) )
{}
Обратите внимание, что tx( forward<A>(a) )
может вызывать explicit
конструкторы. Поскольку этот шаблонный конструктор pair
не помечен как явный, мы должны ограничить его, чтобы не выполнять явные преобразования внутри при инициализации его членов данных. Поэтому мы используем is_convertible
:
template<class U, class V,
std::enable_if_t<std::is_convertible<U&&, T1>::value &&
std::is_convertible<V&&, T2>::value>
>
constexpr pair(U&& u, V&& v)
: t1( forward<U>(u) ), t2( forward<V>(v) )
{}
В случае OP нет неявного преобразования: тип не может быть скопирован, и это делает тест, который определяет неявную конвертируемость, некорректным:
// v is any expression of type 'int'
foobar f = v; // definition of implicit convertibility
Эта форма инициализации копии в соответствии со Стандартом создает временное временное имя, инициализированное с помощью v
:
foobar f = foobar(v);
Где правая часть должна пониматься как неявное преобразование (поэтому нельзя вызывать explicit
конструкторы). Однако для этого необходимо скопировать или переместить временный файл с правой стороны в f
(до С++ 1z, см. P0135r0).
Подводя итог: int
не является неявно конвертируемым в foobar
из-за способа определения неявной конвертируемости, который требует подвижности, потому что RVO не является обязательным. pair<int, foobar>
не может быть построена из {1, 2}
потому что этот шаблон конструктора pair
не является explicit
и, следовательно, требует неявных преобразований.
Лучшее решение проблемы explicit
и неявного преобразования, представленной в разделе " Улучшения pair
и tuple
заключается в использовании explicit
волшебства:
Конструктор является explicit
тогда и только тогда, когда is_convertible<U&&, first_type>::value
равно false
или is_convertible<V&&, second_type>::value
равно false
.
С этим изменением мы можем ослабить ограничение неявной конвертируемости (is_convertible
) на "явную конвертируемость" (is_constructible
). По сути, мы получаем следующий шаблон конструктора в этом случае:
template<class U, class V,
std::enable_if_t<std::is_constructible<U&&, int>::value &&
std::is_constructible<V&&, foobar>::value>
>
explicit constexpr pair(U&&, V&&);
Что достаточно неограниченно, чтобы сделать std::pair<int, foobar> p{1, 2};
действительный.
Ответ 2
Тестирование вашего кода с удалением конструктора копии, я получаю
[h:\dev\test\0082]
> g++ foo.cpp
In file included from h:\bin\mingw\include\c++\4.8.2\utility:70:0,
from foo.cpp:1:
h:\bin\mingw\include\c++\4.8.2\bits\stl_pair.h: In instantiation of 'constexpr std::pair::pair(_U1&&, const _T2&) [with _U1 = int; <template-parameter-2-2> = void; _T1 = int; _T2 = foobar]':
foo.cpp:12:34: required from here
h:\bin\mingw\include\c++\4.8.2\bits\stl_pair.h:134:45: error: use of deleted function 'foobar::foobar(const foobar&)'
: first(std::forward<_U1>(__x)), second(__y) { }
^
foo.cpp:6:5: error: declared here
foobar(const foobar&) = delete;
^
[h:\dev\test\0082]
> cl foo.cpp
foo.cpp
[h:\dev\test\0082]
> _
Указанный конструктор
pair(_U1&&, const _T2&)
не указан стандартом.
Приложение: как показано ниже, код работает просто отлично, только стандартные конструкторы, определенные для парного класса:
#include <utility>
struct foobar
{
foobar(int) { } // implicit conversion
foobar(const foobar&) = delete;
};
namespace bah {
using std::forward;
using std::move;
struct Piecewise_construct_t {};
template <class T1, class T2>
struct Pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
//Pair(const Pair&) = default;
//Pair(Pair&&) = default;
/*constexpr*/ Pair(): first(), second() {}
Pair(const T1& x, const T2& y)
: first( x ), second( y )
{}
template<class U, class V> Pair(U&& x, V&& y)
: first( forward<U>( x ) ), second( forward<V>( y ) )
{}
template<class U, class V> Pair(const Pair<U, V>& p)
: first( p.first ), second( p.second )
{}
template<class U, class V> Pair(Pair<U, V>&& p)
: first( move( p.first ) ), second( move( p.second ) )
{}
//template <class... Args1, class... Args2>
//Pair(Piecewise_construct_t,
//tuple<Args1...> first_args, tuple<Args2...> second_args);
//
//Pair& operator=(const Pair& p);
//template<class U, class V> Pair& operator=(const Pair<U, V>& p);
//Pair& operator=(Pair&& p) noexcept(see below);
//template<class U, class V> Pair& operator=(Pair<U, V>&& p);
//void swap(Pair& p) noexcept(see below);
};
}
auto main()
-> int
{
bah::Pair<int, foobar> p{1, 2};
};
[h:\dev\test\0082]
> g++ bar.cpp
[h:\dev\test\0082]
> _
ВАЖНО ERRATA.
Поскольку @dyb указывает на комментарии, в то время как стандартное предложение "требует" относится к std::is_constructible
(элементы пары должны быть конструктивными из аргументов), предложение "примечания", следуя разрешению Отчет о дефектах 811, относится к конвертируемости:
С++ 11 §20.3.2/8:
"Замечания: если U
неявно конвертируется в first_type
или V
, неявно конвертируется в second_type
, этот конструктор не должен участвовать в разрешении перегрузки."
Итак, хотя это, возможно, теперь является дефектом в стандарте, с формальной точки зрения код не должен компилироваться.