c++templatesconstantsfunction-templates-overloading

Why does this code not use the template specialization or fail with ambiguous calls?


This code prints Upper, although I would want it to print Lower:

#include <concepts>
#include <iostream>

struct X {
    explicit operator bool() const { return true; }
};

template<class T>
    requires std::constructible_from<T, X>
const T function()  {
    std::cout << "Upper";
    return T();
}

template<class T>
T function()  {
    std::cout << "Middle";
    return T();
}

template<>
bool function<bool>()  {
    std::cout << "Lower";
    return true;
}

int main() {
    function<bool>();
}

After all, while I see that Upper works (as does Middle), Lower is a template specialization for bool that should be preferred. If it is not using that overload, I wonder why the compiler does not fail with some kind of "ambiguity" error.

By trial-and-error, I have found that removing const from the return value of Upper seems to do the trick to call Lower. Why?


Solution

  • Function templates can be overloaded and specialized. (unlike class templates, they cannot be partially specialized).

    Overloading happens when there is more than one function template or function in play. Then overload resolution rules are used to pick which one is found when you call it.

    Specialization only changes what after a template is selected. If a given template function has a (manual) specialization, its implementation is used.

    In your case you have 2 overloads:

    template<class T>
    requires std::constructible_from<T, X>
    const T function();
    
    template<class T>
    T function();
    

    and one specialization of the 2nd overload:

    template<>
    bool function<bool>();
    

    Given this, it is pretty clear what is going on; the compiler looks at those two overloads, see quite clearly the first is more-specialized (narrower), and selects the first overload. It never even considers the specialization you wrote: function template full specializations have zero impact on overload resolution.

    (PS: returning const T is code smell).

    An obvious solution is:

    template<class T>
    requires std::constructible_from<T, X>
    T function();
    
    template<class T>
    requires std::is_same_v<T, bool>
    bool function();
    
    template<class T>
    T function();
    

    which at least gets us this error message:

    <source>:29:5: error: call to 'function' is ambiguous
       29 |     function<bool>();
    [... details that point at the 2 requires templates as the ambiguous ones ...]
    

    basically, neither is "more specialized".

    We can force the compiler to understand one is narrower:

    template<class T>
    requires std::is_same_v<T, bool> && std::constructible_from<T, X>
    bool function();
    

    and the result works.

    The other approach is to remove the const T return value. Then

    template<>
    bool function<bool>()
    

    becomes a valid specialization of both of the overloads. So while the first overload is selected, when the compiler looks for the implementation it uses the function<bool> specialization.

    I will note how fragile this is. I find the risks of using template specializations to be too high, as subtle things (like const in your example) cause things to disconnect unexpectedly.

    If I want to dispatch on a type, I'll change that type to a tag argument:

    template<class T>
    T function() { return function_impl( tag_v<T> ); }
    

    then I can do everything at overload resolution time.

    template<class T>
    requires std::constructible_from<T, X>
    T function_impl(tag_t<T>)  {
      std::cout << "Upper";
      return T();
    }
    
    template<class T>
    T function_impl(tag_t<T>)  {
      std::cout << "Middle";
      return T();
    }
    
    inline bool function_impl(tag_t<bool>) {
      std::cout << "Lower";
      return true;
    }
    

    The result isn't as fragile, and I find reasoning about overload resolution order to be easier than "is this a valid specialization" rules.