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

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

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

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 может быть таким. Теперь, инициализируя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)

По разным причинам единственный доступный вариант прямой инициализации с неявным преобразованием. Тем не менее, это именно то, где неявное преобразованиеопасно. 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)

Ваш ответ на вопрос