c++overload-resolutionfunction-templates-overloading

Counterintuitive template overload resolution


Consider the following code:

#include <tuple>
#include <type_traits>
#include <cstddef> 

struct A {
  using type = std::tuple<int, float>;
};

struct B : A {
  using type = std::tuple<int, float, double>;
};

template <std::size_t N>
std::tuple_element_t<N, typename B::type>
foo(std::integral_constant<std::size_t, N>, B); // (1)

template <std::size_t N>
std::tuple_element_t<N, typename A::type>
foo(std::integral_constant<std::size_t, N>, A); // (2)

using type = decltype(foo(std::integral_constant<std::size_t, 2>{}, B{}));

int main() {}

It does not compile, because the compiler selects the overload (2).
Why does it happen? Isn't (1) a better match?

Tested with both gcc and clang, C++20.


Solution

  • It doesn't actually pick the second overload. The problem is that std::tuple_element_t apparently is not SFINAE-friendly.

    Merely checking that overloaded requires calling std::tuple_element_t<2, std::tuple<int, float>>, which fails on a static_assert, which is not something SFINAE can recover from.

    Make your own wrapper that fails in a SFINAE-friendly way:

    template <std::size_t I, typename T>
    requires (I < std::tuple_size<T>::value)
    using my_tuple_element = std::tuple_element_t<I, T>;
    

    And use my_tuple_element instead of std::tuple_element_t.


    Side note: std::tuple_size_v<T> is infamously not SFINAE-friendly (fails on non-tuple-like types), but std::tuple_size<T>::value is.