16

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?

Community
  • 1
  • 1
iavr
  • 7,547
  • 1
  • 18
  • 53
  • 1
    I wonder about a `const&&` method which would resolve the ambiguity between `const&` and `&&`. For safety, it could still return a `const&`, but I suspect making it return `&&` (with a `const_cast`) would be as safe seeing as a `const` temporary seems meaningless. – Matthieu M. May 10 '14 at 14:59
  • 1
    @MatthieuM If I remember well, I have tried this to no avail. It appears that it only adds to ambiguities. Unfortunately, I have given up conversion operators altogether for now. – iavr May 10 '14 at 18:02
  • @iavr I've read the question a few times now and unfortunately I still can't figure out for each case what the desired functionality is. Which of all the cases you've enumerated do you want to work? – Mark B Jul 01 '14 at 17:11
  • 1
    @MarkB Well, It would be nice if implicit conversion (everything containing `_impl`) worked in all cases (even if dangerous). Otherwise, I'd expect at least explicit conversion to work with direct initialization (everything containing `direct_` and `_expl`). Sorry if this wasn't clear. – iavr Jul 02 '14 at 12:32

1 Answers1

4

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); }
};
alexk7
  • 2,721
  • 1
  • 21
  • 18
  • 1
    By answering, I finally found a use case for const&& :) – alexk7 Jul 02 '14 at 21:34
  • Very interesting, thanks. By dropping the explicit type specification, `cast()` could even reduce to `operator*` or something. By the way, wouldn't just `&&` work for all conversion operators? Do you really need `const&&`? The type of reference is only relevant to the `A` object, to the type of data held by `A`, and to the type being cast to; not to the proxy object itself, right? – iavr Jul 03 '14 at 08:12
  • 1
    Well, the decision to use const&& on the proxy class conversion operator come from two things: First, I use an r-value reference because I want the proxy class to only be used as a temporary. Second, I don't modify the proxy class to accomplish its mission, so it can be const, so it *should* be const :) No more complicated than that. – alexk7 Jul 04 '14 at 02:19