Search code examples
c++c++20c++-conceptsstdtuple

std::tuple data member in a concept


Continuing my studies in C++ concepts, I would like to define a std::tuple<t...> data member in a concept.

In the following code:

#include <concepts>
#include <iostream>
#include <tuple>

template <typename t>
concept c1 = requires(t p_t) {
  { p_t.i } -> std::same_as<int &>;
};

template <typename t, typename... c1>
concept c3 = requires(t p_t) {
  { p_t.cs } -> std::same_as<std::tuple<c1...> &>; // ERROR 1
};

template <c1... t_c1> struct a1 { std::tuple<t_c1...> cs; };

struct a2 {
  int i{-9};
};

struct a3 {
  int i{8};
};

void f(c3 auto &p_c3) { std::cout << std::get<0>(p_c3.cs).i << std::endl; } // ERROR 2

int main() {

  a1<a2, a3> _a;

  f(_a);  // ERROR 3

  return 0;
}

But there are errors:

ERROR 1 (the cause of the other errors)

main.cpp:12:17: Because type constraint 'std::same_as<std::tuple<a2, a3> &, std::tuple<> &>' was not satisfied:
main.cpp:31:3: Because type constraint 'std::same_as<std::tuple<a2, a3> &, std::tuple<> &>' was not satisfied:

ERROR 2:

main.cpp:25:8: Because 'a1<a2, a3>' does not satisfy 'c3'
main.cpp:31:3: Because 'a1<a2, a3>' does not satisfy 'c3'

ERROR 3:

main.cpp:31:3: No matching function for call to 'f'
main.cpp:25:6: candidate template ignored: constraints not satisfied [with p_c3:auto = a1<a2, a3>]
main.cpp:25:8: because 'a1<a2, a3>' does not satisfy 'c3'
main.cpp:12:17: because type constraint 'std::same_as<std::tuple<a2, a3> &, std::tuple<> &>' was not satisfied:
/usr/include/c++/11/concepts:63:9: note: because '__detail::__same_as<std::tuple<a2, a3> &, std::tuple<> &>' evaluated to false
/usr/include/c++/11/concepts:57:27: note: because 'std::is_same_v<std::tuple<a2, a3> &, std::tuple<> &>' evaluated to false

If I change the line { p_t.cs } -> std::same_as<std::tuple<c1...> &>; to { p_t.cs }; the errors disappear.

So, what is the correct way to define a std::tuple data member in a concept?


Solution

  • For concepts there is no deduction of the type parameters, so given a parameter of type T passed to f the concept the compiler is checking is c3<T>, but for a parameter of type a1<a2, a3> the concept that would apply would be c3<T, a2, a3>, i.e. the function signature would need to be

    void f(c3<a2, a3> auto &p_c3);
    

    Note that for actually implementing something like c3, you'd need to deduce the template parameters which requires to be able to check them against c1, so you'd need to basically go with pre-concept strategies for this:

    // helper to enforce IsC1TupleHelper is only used in an unevaluated context
    template<class ...Ts>
    constexpr bool AlwaysFalse = false;
    
    // helper function for deducing the template parameters of std::tuple
    template<c1 ... Ts>
    std::true_type IsC1TupleHelper(std::tuple<Ts...>const&)
    {
        static_assert(AlwaysFalse<Ts...>, "IsC1TupleHelper is only allowed to be used in an unevaluated context");
    }
    
    // helper function fallback for types other than tuples of c1
    template<class T>
    std::false_type IsC1TupleHelper(T const&)
    {
        static_assert(AlwaysFalse<T>, "IsC1TupleHelper is only allowed to be used in an unevaluated context");
    }
    
    // c3 with a better(?) name
    template<class T>
    concept CsMemberIsTupleOfC1 = requires(T t)
    {
        { IsC1TupleHelper(t.cs) } -> std::same_as<std::true_type>;
    };
    
    void f(CsMemberIsTupleOfC1 auto& p_c3)
    {
        std::cout << std::get<0>(p_c3.cs).i << std::endl;
    }
    

    You need to be aware though that this concept won't prevent the user from passing a parameter of a type to f that results in a compiler error, e.g.

    a1<> _a2;
    f(_a2);
    

    fails compilation of the specialization of f, since you try to access the first element of a tuple that doesn't have any elements using std::get<0>.

    You the following implementation of c3 would probably be more suitable and wouldn't require deducing the type parameters of the tuple:

    // checks if the first element type of the tuple matches c1
    template<class T>
    concept FirstTupleElementIsC1 = c1<std::tuple_element_t<0, std::remove_cvref_t<T>>>;
    
    template <typename T>
    concept c3 = requires(T p_t) {
        { p_t.cs } -> FirstTupleElementIsC1;
    };
    

    or

    template <typename T>
    concept c3 = requires(T p_t) {
        { std::get<0>(p_t.cs) } -> c1;
    };