Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructor is always used instead of explicit conversion operator

I have the following class:

template <typename T1>
class Foo {
public:
    Foo(T1* obj) : obj(obj) {}

    template <typename T2>
    Foo(const Foo<T2>& other) : obj(other.obj) {}

    template <typename T2>
    explicit operator Foo<T2>() {
        return Foo<T2>(static_cast<T2*>(obj));
    }
    T1* obj;
};

The intention of the second constructor is that an implicit conversion from Foo<X> to Foo<Y> is allowed iff an implicit conversion from X* to Y* is allowed.

The conversion operator is there to allow an explicit conversion from Foo<X> to Foo<Y> using an explicit conversion from X* to Y*.

But I noticed that the conversion operator never gets used. The compiler always uses the second constructor even when I do an explicit cast. This causes an error if an implicit conversion of the underlying types is not possible.

The following code can be used to test the class above.

class X {};
class Y : public X {};

int main() {
    Y obj;
    Foo<Y> y(&obj);
    Foo<X> x = y; // implicit cast works as expected.
    // y = x; // implicit conversion fails (as expected).
    // y = static_cast<Foo<Y>>(x); // conversion fails because constructor is
                                   // called instead of conversion operator.
}

Is there a way to cause the compiler to use the conversion operator for explicit conversions?

like image 635
dennis95 Avatar asked Jul 16 '17 15:07

dennis95


2 Answers

For static_cast<Foo<Y>>(x);, you're trying to construct a Foo<Y> from x (which is a Foo<X>) directly, for such context the converting constructor is preferred to conversion function.

(emphasis mine)

If both conversion functions and converting constructors can be used to perform some user-defined conversion, the conversion functions and constructors are both considered by overload resolution in copy-initialization and reference-initialization contexts, but only the constructors are considered in direct-initialization contexts.

struct To {
    To() = default;
    To(const struct From&) {} // converting constructor
};

struct From {
    operator To() const {return To();} // conversion function
};

int main()
{
    From f;
    To t1(f); // direct-initialization: calls the constructor
// (note, if converting constructor is not available, implicit copy constructor
//  will be selected, and conversion function will be called to prepare its argument)
    To t2 = f; // copy-initialization: ambiguous
// (note, if conversion function is from a non-const type, e.g.
//  From::operator To();, it will be selected instead of the ctor in this case)
    To t3 = static_cast<To>(f); // direct-initialization: calls the constructor
    const To& r = f; // reference-initialization: ambiguous
}

You can make the conversion constructor to be discarded from the overload set for this case by SFINAE; i.e. make it valid only when the implicit conversion of the underlying pointers is allowed.

template <typename T2, typename = std::enable_if_t<std::is_convertible<T2*, T1*>::value>>
Foo(const Foo<T2>& other) : obj(other.obj) {}

LIVE

like image 198
songyuanyao Avatar answered Oct 21 '22 21:10

songyuanyao


Take a look at the following quote from the standard

Static cast [expr.static.cast/4]

An expression e can be explicitly converted to a type T using a static_cast of the form static_cast<T>(e) if the declaration T t(e); is well-formed, for some invented temporary variable t (8.5). The effect of such an explicit conversion is the same as performing the declaration and initialization and then using the temporary variable as the result of the conversion. The expression e is used as a glvalue if and only if the initialization uses it as a lvalue.

This basically says that a constructor is preferred over a conversion operator, and that is what happens.

like image 21
Curious Avatar answered Oct 21 '22 20:10

Curious