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

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 типов?

4b9b3361

Ответ 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, этот конструктор не должен участвовать в разрешении перегрузки."

Итак, хотя это, возможно, теперь является дефектом в стандарте, с формальной точки зрения код не должен компилироваться.