Home > Blockchain >  Why is function call treated as instantiation when I cast in template arguments?
Why is function call treated as instantiation when I cast in template arguments?

Time:07-10

I've got the following code:

template <bool condition>
struct enable_if { };

template <>
struct enable_if<true> { using type = bool; };

template <typename T>
class is_callable {
    using Yes = char[1];
    using No = char[2];

    template <typename U> static Yes& filter(decltype(&U::operator()));
    template <typename U> static No& filter(...);

public:
    constexpr operator bool() { return sizeof(filter<T>(nullptr)) == sizeof(Yes); }
};

template <typename Lambda, typename enable_if<is_callable<Lambda>{}>::type = true>
void doSomethingWithLambda(Lambda func) {
    func();
}

int main() {
    doSomethingWithLambda([]() { });
}

The important part is the enable_if<is_callable<Lambda>{}>::type part. One is forced to instantiate is_callable<Lambda> with {} because if one were to use (), C would mistake it for a function call.

Feel free to correct me if I'm wrong, but as far as I know, C assumes it is a function in the () case so that the type of expression isn't determined after the time of writing, saving everyone a headache. What I mean by that is, assuming you had a function version and a class version of is_callable (separated by SFINAE using enable_if or something along those lines), the type Lambda could determine the true meaning of (), either a function call or an instantiation. Like I said, as far as I know, C wants to avoid this confusion, so it assumes function call and fails if such a function does not exist.

Based on the assumptions above, the following shouldn't work:

enable_if<(bool)is_callable<Lambda>()>::type

What does it matter if I cast the result of the function call (never mind that functions couldn't even be evaluated in this context)? Why is this suddenly treated as an instantiation instead of a function call?

CodePudding user response:

No, your understanding is not correct.

Firstly, a name can't refer to both a class template and a function template. If that happens the program is ill-formed. (And defining both in the same scope is not allowed to begin with.)

Secondly, is_callable<Lambda>() as template argument is not a function call to begin with. It is a function type. It is the type of a function which has no parameters and returns a is_callable<Lambda>.

When the compiler parses a template argument, it can interpret it in two ways: Either as a type or as an expression (or as a braced-init-list), because template parameters can be type parameters or non-type parameters.

When the compiler reads is_callable<Lambda>() it notices that is_callable is a class template and then realizes that is_callable<Lambda> is therefore a type. If you have a type, let's shorten it to T, then T() can either be syntax representing the type of a function returning T and taking no arguments, or it can be an expression formed from one single functional notation explicit cast (which you imprecisely call "instantiation").

There is no way to differentiate these two cases in the context, but the compiler needs to know whether this is a type template argument or a non-type template argument. So there is a rule saying that such ambiguities are always resolved in favor of a type.

If is_callable was a function template instead, there would be no ambiguity, because then is_callable<Lambda> is not a type and therefore is_callable<Lambda>() cannot be a function type. It must be a function call instead and therefore an expression and non-type template argument.

When you write (bool)is_callable<Lambda>() this is not valid syntax for a type and therefore there is no ambiguity. It is a non-type template argument and an expression. And is_callable<Lambda>() is a funcational notation explicit cast because is_callable<Lambbda> is a type. If is_callable was a function template instead of a class template, then it would be a function call.

  • Related