This is a follow-up of Explicit ref-qualified conversion operator templates in action. I have experimented with many different options and I am giving some results here in an attempt to see if there is any solution eventually.
Say a class (e.g. any) needs to provide conversion to any possible type in a convenient, safe (no surprises) way that preserves move semantics. I can think of four different ways.
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&;
};
The most problematic cases are to initialize an object or an rvalue reference to an object, given a temporary or an rvalue reference. Function calls work in all cases (I think) but I find them too verbose:
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)
So, I next experiment with conversion operators:
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)
It appears that the const&
overload is callable on an rvalue, which gives ambiguities, leaving copy-initialization with an implicit conversion as the only option.
However, consider the following less trivial class:
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>;
which provides generic implicit or explicit constructors depending on whether the input arguments can be implicitly or explicitly converted to a certain type. Such logic is not that exotic, e.g. some implementation of std::tuple
can be like that. Now, initializing a D
gives
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)
For different reasons, the only available option direct-initialization with an implicit conversion. However, this is exactly where implicit conversion is dangerous. b
might actually contain a D
, which may be a kind of container, yet the working combination is invoking D
's constructor as an exact match, where b
behaves like a fake element of the container, causing a runtime error or disaster.
Finally, let's try to initialize an rvalue reference:
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)
It appears that every use case has its own requirements and there is no combination that might work in all cases.
What's worse, all above results are with clang 3.3; other compilers and versions give slightly different results, again with no universal solution. For instance: live example.
So: is there any chance something might work as desired or should I give up conversion operators and stick with explicit function calls?
The C++ standard unfortunately does not have any special rule to resolve this particular ambiguity. The problem come from the fact that you are trying to overload on 2 different things: the type that the compiler is trying to convert to; and the kind of reference from which you are trying to convert from.
By introducing proxy classes, you can split the resolution in 2 steps. Step 1: decide if it's an r-value reference, an l-value reference, or a const l-value reference. Step 2: convert to any type, keeping the decision made in step 1 about the kind of reference. That way, you can use your solution with a cast() function but save you from having to specify the type:
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); }
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With