Search code examples
c++templatestuplesc++20c++-concepts

One template to std::get them all


For use in other templates, I want to have a function like std::get for a tuple which returns an instance of a default type when the get call is invalid; the kind of

template<size_t I, typename T> auto get_or(T const& t)
{
  // Pseudocode
  return ("if std::get<I>(t) is valid") ? std::get<I>(t) : std::monostate{};
}

But I failed, and I wonder whether it is due to my lack of experience with C++ concepts, or maybe a bug in the compiler (Visual Studio 2022 version 17.8.4), or maybe it simply is not possible.

This is what I tried:

I defined a type trait and a concept that tell me whether T is a tuple:

template<typename T> struct is_tuple : std::false_type {};
template<typename... Ts> struct is_tuple<std::tuple<Ts...>> : std::true_type {};

namespace test_concepts
{
    template<typename T> concept is_tuple = requires { ::is_tuple<T>::value; };
}

Next, I want to answer the question "given a tuple t and a compile-time constant I, would std::get<I>(t) be valid?

template<size_t I, typename T> constexpr bool can_get (T const&) { return false; }
template<size_t I, typename T> requires test_concepts::is_tuple<T>
constexpr bool can_get(T const& t)
{
    return (I < std::tuple_size_v<std::remove_cvref_t<decltype(t)>>);
}
namespace test_concepts
{
    template<size_t I, typename T> concept can_get = requires(T t) { ::can_get<I>(t); };
}


// Test code
std::tuple<> t0;
std::tuple<int> t1;
std::tuple<double, float> t2;

static_assert(!can_get<0>(t0)); static_assert(!can_get<1>(t0)); static_assert(!can_get<2>(t0));
static_assert( can_get<0>(t1)); static_assert(!can_get<1>(t1)); static_assert(!can_get<2>(t1));
static_assert( can_get<0>(t2)); static_assert( can_get<1>(t2)); static_assert(!can_get<2>(t2));

This works. However, when I do the final step, I receive a compile-time error.

template<size_t I, typename T> auto get_or(T const& t)
{
    return std::monostate{};
}
template<size_t I, typename T> requires test_concepts::can_get<I, T>
auto get_or(T const& t)
{
    return std::get<I>(t);
}

// Test code
std::tuple<> t0;
std::tuple<int> t1;
auto x00 = get_or<0>(t0);
// I expect x00 to be of type std::monostate, but instead this error is displayed:
// error C2338: static_assert failed: 'tuple index out of bounds'
auto x10 = get_or<0>(t1);
// Context help shows that x10 is of type int as expected
auto x11 = get_or<1>(t1);
// error C2338 again for x11

Any ideas? - Thanks!


Solution

  • You are misusing the requires-clause.

    The requires-clause only checks the validity of the expression without actually evaluating it, so your concept is always satisfied.

    You can use nested requires to evaluate the expression, e.g.

    template<typename T> concept is_tuple = requires { requires ::is_tuple<T>::value; };
    

    Or even simpler

    template<typename T> concept is_tuple = ::is_tuple<T>::value;
    

    The same goes for test_concepts::can_get.