Типичные шаблоны операторов преобразования и семантика перемещения: любое универсальное решение?

Это продолжение Явных рефлексивных шаблонов операторов преобразования в действии. Я экспериментировал со многими различными вариантами, и я приношу некоторые результаты здесь, пытаясь понять, есть ли какое-либо решение в конце концов.

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

struct A
{
    // explicit conversion operators (nice, safe?)
    template<typename T> explicit operator T&&       () &&;
    template<typename T> explicit operator T&        () &;
    template<typename T> explicit operator const T&  () const&;

    // explicit member function (ugly, safe)
    template<typename T> T&&       cast() &&;
    template<typename T> T&        cast() &;
    template<typename T> const T&  cast() const&;
};

// explicit non-member function (ugly, safe)
template<typename T> T&&       cast(A&&);
template<typename T> T&        cast(A&);
template<typename T> const T&  cast(const A&);

struct B
{
    // implicit conversion operators (nice, dangerous)
    template<typename T> operator T&&       () &&;
    template<typename T> operator T&        () &;
    template<typename T> operator const T&  () const&;
};

Наиболее проблематичными являются инициализация объекта или ссылки на rvalue для объекта с учетом временного или ссылочного значения rvalue. Функциональные вызовы работают во всех случаях (я думаю), но я нахожу их слишком подробными:

A a;
B b;

struct C {};

C member_move = std::move(a).cast<C>();  // U1. (ugly) OK
C member_temp = A{}.cast<C>();           // (same)

C non_member_move(cast<C>(std::move(a)));  // U2. (ugly) OK
C non_member_temp(cast<C>(A{}));           // (same)

Итак, следующий эксперимент с операторами преобразования:

C direct_move_expl(std::move(a));  // 1. call to constructor of C ambiguous
C direct_temp_expl(A{});           // (same)

C direct_move_impl(std::move(b));  // 2. call to constructor of C ambiguous
C direct_temp_impl(B{});           // (same)

C copy_move_expl = std::move(a);  // 3. no viable conversion from A to C
C copy_temp_expl = A{};           // (same)

C copy_move_impl = std::move(b);  // 4. OK
C copy_temp_impl = B{};           // (same)

Похоже, что перегрузка const& может быть вызвана на rvalue, что дает неоднозначность, оставляя копию-инициализацию с неявным преобразованием в качестве единственного варианта.

Однако рассмотрим следующий менее тривиальный класс:

template<typename T>
struct flexi
{
    static constexpr bool all() { return true; }

    template<typename A, typename... B>
    static constexpr bool all(A a, B... b) { return a && all(b...); }

    template<typename... A>
    using convert_only = typename std::enable_if<
        all(std::is_convertible<A, T>{}...),
    int>::type;

    template<typename... A>
    using explicit_only = typename std::enable_if<
        !all(std::is_convertible<A, T>{}...) &&
        all(std::is_constructible<T, A>{}...),
    int>::type;

    template<typename... A, convert_only<A...> = 0>
    flexi(A&&...);

    template<typename... A, explicit_only<A...> = 0>
    explicit flexi(A&&...);
};

using D = flexi<int>;

который предоставляет общие неявные или явные конструкторы в зависимости от того, могут ли входные аргументы быть неявно или явно преобразованы в определенный тип. Такая логика не так экзотична, например. может быть и такая реализация std::tuple. Теперь инициализация a D дает

D direct_move_expl_flexi(std::move(a));  // F1. call to constructor of D ambiguous
D direct_temp_expl_flexi(A{});           // (same)

D direct_move_impl_flexi(std::move(b));  // F2. OK
D direct_temp_impl_flexi(B{});           // (same)

D copy_move_expl_flexi = std::move(a);  // F3. no viable conversion from A to D
D copy_temp_expl_flexi = A{};           // (same)

D copy_move_impl_flexi = std::move(b);  // F4. conversion from B to D ambiguous
D copy_temp_impl_flexi = B{};           // (same)

По разным причинам единственная доступная опция direct-initialization с неявным преобразованием. Однако именно здесь неявное преобразование опасно. b может содержать D, который может быть своего рода контейнером, но рабочая комбинация вызывает конструктор D как точное совпадение, где b ведет себя как поддельный элемент контейнера, вызывая время выполнения ошибки или катастрофы.

Наконец, попробуйте инициализировать ссылку rvalue:

D&& ref_direct_move_expl_flexi(std::move(a));  // R1. OK
D&& ref_direct_temp_expl_flexi(A{});           // (same)

D&& ref_direct_move_impl_flexi(std::move(b));  // R2. initialization of D&& from B ambiguous
D&& ref_direct_temp_impl_flexi(B{});           // (same)

D&& ref_copy_move_expl_flexi(std::move(a));  // R3. OK
D&& ref_copy_temp_expl_flexi(A{});           // (same)

D&& ref_copy_move_impl_flexi = std::move(b);  // R4. initialization of D&& from B ambiguous
D&& ref_copy_temp_impl_flexi = B{};           // (same)

Похоже, что каждый вариант использования имеет свои собственные требования, и нет никакой комбинации, которая могла бы работать во всех случаях.

Что хуже, все вышеприведенные результаты с clang 3.3; другие компиляторы и версии дают несколько иные результаты, опять же без универсального решения. Например: живой пример.

Итак: есть ли шанс, что что-то может работать по желанию или я должен отказаться от операторов преобразования и придерживаться явных вызовов функций?

Ответы

Ответ 1

В стандарте С++, к сожалению, нет специального правила для устранения этой конкретной двусмысленности. Проблема возникает из-за того, что вы пытаетесь перегрузить две разные вещи: тип, к которому пытается конвертировать компилятор; и вид ссылки, с которой вы пытаетесь преобразовать.

Вводя прокси-классы, вы можете разделить разрешение на 2 шага. Шаг 1: определите, является ли это ссылкой на r-значение, ссылкой l-value или ссылкой const l-value. Шаг 2: конвертировать в любой тип, сохраняя решение, сделанное на шаге 1, о типе ссылки. Таким образом, вы можете использовать свое решение с функцией cast(), но не можете указать тип:

struct A
{
    class A_r_ref
    {
        A* a_;
    public:
        A_r_ref(A* a) : a_(a) {}
        template <typename T> operator T&&() const&&;
    };

    struct A_ref
    {
        A* a_;
    public:
        A_ref(A* a) : a_(a) {}
        template <typename T> operator T&() const&&;
    };

    struct A_const_ref
    {
        const A* a_;
    public:
        A_const_ref(const A* a) : a_(a) {}
        template <typename T> operator const T&() const&&;
    };

    A_r_ref cast() && { return A_r_ref(this); }
    A_ref cast() & { return A_ref(this); }
    A_const_ref cast() const& { return A_const_ref(this); }
};