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.
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.