Search code examples
c++templatesc++17enable-if

Using std::enable_if with constexpr argument for overload resolution doesn't work as expected


I'm trying to add function overload for a function template based on the type of one of the template parameters and passing an argument defined as consexpr somewhere else to aid in overload resolution.

Here's a code sample for what I'm trying to achieve:

struct use_my_impl_t {};
constexpr use_my_impl_t use_my_impl;

template<
        typename T,
        typename std::enable_if<std::is_same<T, use_my_impl_t>::value>::type * = nullptr>
void foo(int i, T&& t)
{
    std::cout << "Using my impl: " << --i << std::endl;
}

template<
    typename T,
    typename std::enable_if<!std::is_same<T, use_my_impl_t>::value>::type * = nullptr>
    void foo(int i, T&& t)
{
    std::cout << "Using default impl: " << ++i << std::endl;
}

So when I pass an instance of use_my_impl_t as a second argument it should use my implementation, and for all other cases it should use the default impl. The result is not what I would expect though. If I call like like this:

constexpr use_my_impl_t use_my_impl;

foo(42, use_my_impl_t{});
foo(42, 2);
foo(42, use_my_impl);

it prints

Using my impl: 41
Using default impl: 43
Using default impl: 43

Why the last statement doesn't invoke my implementation despite an instance of use_my_impl_t being passed?


Solution

  • The issue here is what T is being deduced as. In foo(42, use_my_impl_t{});, use_my_impl_t{} is an rvalue so T gets deduced as use_my_impl_t and the parameter becomes use_my_impl_t&&.

    With foo(42, use_my_impl);, use_my_impl is an lvalue. That means T is deduced as use_my_impl_t & to preserve that lvalueness. That means the parameter becomes use_my_impl_t& && which collapses to use_my_impl_t&.

    This is what is causing std::is_same<T, use_my_impl_t>::value not to work, since a use_my_impl_t& is not the same as a use_my_impl_t. To fix this you can use std::decay_t to remove the reference and cv-qualification of the type. That would give you

    template<
        typename T,
        typename std::enable_if<std::is_same<std::decay_t<T>, use_my_impl_t>::value>::type * = nullptr>
    void foo(int i, T&& t)
    {
        std::cout << "Using my impl: " << --i << std::endl;
    }
    
    template<
        typename T,
        typename std::enable_if<!std::is_same<std::decay_t<T>, use_my_impl_t>::value>::type * = nullptr>
        void foo(int i, T&& t)
    {
        std::cout << "Using default impl: " << ++i << std::endl;
    }
    

    which then outputs

    Using my impl: 41
    Using default impl: 43
    Using my impl: 41
    

    using your driver code.