Это продолжение Явные 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&
Перегрузка вызывается для r-значения, что дает неоднозначности, оставляя инициализацию копирования с неявным преобразованием в качестве единственной опции.
Однако рассмотрим следующий менее тривиальный класс:
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; другие компиляторы и версии дают немного другие результаты, опять же, без универсального решения. Например: живой пример.
Так: есть ли вероятность, что что-то может работать как нужно, или я должен отказаться от операторов преобразования и придерживаться явных вызовов функций?
К сожалению, в стандарте C ++ нет специального правила для разрешения этой конкретной неоднозначности. Проблема заключается в том, что вы пытаетесь перегрузить две разные вещи: тип, в который пытается преобразовать компилятор; и тип ссылки, из которой вы пытаетесь преобразовать.
Вводя прокси-классы, вы можете разделить разрешение на 2 шага. Шаг 1: определите, является ли это ссылкой r-значения, ссылкой l-значения или константной ссылкой l-значения. Шаг 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); }
};