Search code examples
c++language-lawyerimplicit-conversiondereference

C++ dereference happens after implicit conversion


Today I saw that when trying to dereference an argument in a function call, the implicit conversion happens before the actual dereference operation (I believe only if the original type does not support dereference).

This observation can be seen here:

struct A{};


struct C
{
    C(const A&)
    {

    }
};


C operator*(const C&);

double f(C);


template <typename T>
struct t
{
    static const bool value = sizeof(f(**static_cast<T*>(NULL))) == sizeof(double);
};


int main(int argc, const char * argv[]) {
    t<A>::value;
}

Does the C++ standard explicitly mention this behaviour? Thank you.


Solution

  • Let's take a look at this expression:

    f(**static_cast<A*>(NULL))
    

    From inner-most to outer-most, static_cast<A*>(NULL) is an A* (though prefer nullptr to NULL, and also prefer using std::declval<A*>() to get "something of type A*" rather than the old casting of null pointer approach).

    Next, *(a prvalue of type A*) gives you an lvalue of type A. That's just what pointer dereference means and it isn't overloadable. It's nice when things are easy to reason about.

    Next, *(an lvalue of type A). In order to figure out what that means, we transform the call to function-notation, and our set of candidates are:

    The first bullet doesn't find anything, there is no A::operator*(). The second bullet, unqualified lookup on operator*() will find the function C operator*(const C&); because it is in scope. It is a viable candidate, because A is convertible to C via C(A const&). The third bullet has no viable candidate.

    As we only have one viable candidate, it's trivially the best viable candidate - so *(lvalue of type A) gives us a prvalue of type C.

    To specifically answer your question:

    the implicit conversion happens before the actual dereference operation

    Yes, the conversion has to happen in order to resolve the dereference operation. Although it's not actually a "dereference", it's just a unary operator*().

    Lastly, f(prvalue of type C) invokes the one f we have that gives us double.


    Note that in modern C++, I'd suggest writing the check as something closer to:

    template <typename T>
    struct t
        : std::is_same<
            decltype(f(*std::declval<T>())), // <== the type we get when we call f
                                             // on a dereferenced T
            double>
    { };