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

How to write a concept that checks for an inner type in all the types of a std::tuple?


Continuing with my studies on C++ 20 concepts, I am trying to write a concept that would assure that every type in a std::tuple has inner type defined with a specific name, and that all the inners have the same type.

I tried this:

#include <concepts>
#include <string>
#include <tuple>

template <typename t>
concept type_model = requires(t p_t) {
  typename t::inner;
};

template <typename t, typename u>
concept same_inner = requires(t p_t, u p_u) {
  std::is_same_v<typename t::inner, typename u::inner>;
};

template <typename t, typename... t_type_model>
concept container_model = requires(t p_t) {

  requires((same_inner<t_type_model,
                       std::tuple_element_t<0, std::tuple<t_type_model...>>> &&
            ...));
};

struct type_1 {
  using inner = int;
};

struct type_2 {
  using inner = std::string;
};

template <type_model... t_type_models> struct container {};

template <type_model... t_type_model>
void f(container_model<t_type_model...> auto) {}

using containers_X = container<type_1, type_2>;

int main() {

  containers_X _x;

  f(_x);

  return 0;
}

I expected an error of concept not satisfied because type_1::inner and type_2::inner have different types, but no errors were reported.


Solution

  • This code,

    template <typename t, typename... t_type_model>
    concept container_model = requires(t p_t) {
    
      requires((same_inner<t_type_model,
                           std::tuple_element_t<0, std::tuple<t_type_model...>>> &&
                ...));
    };
    

    does not actually test the condition within the second requires clause.

    The correct formulation is

    template <typename t, typename u>
    concept same_inner = std::is_same_v<typename t::inner, typename u::inner>;
    

    or,

    template <typename t, typename u>
    concept same_inner = requires (t i, u j) {
        requires (std::same_as<typename t::inner, typename u::inner>);
    };
    

    This has tripped me up in the past.

    Update

    The container_model concept

    template <typename t, typename... t_type_model>
    concept container_model = requires(t p_t) {
    
      requires((same_inner<t_type_model,
                           std::tuple_element_t<0, std::tuple<t_type_model...>>> &&
                ...));
    };
    

    need to be something like

    template <typename t, typename... t_type_model>
    concept container_model = (same_inner<t, t_type_model> && ...);
    

    At this point, the following assertions should hold:

    static_assert(not same_inner<type_1, type_2>);
    static_assert(not container_model<type_1, type_2>);
    

    When the function f is invoked, it is passed container<type_1, type_2> which qualifies as a container_model because it gets translated to container_model<container<type_t, type_2>> which returns true.

    One possible fix is to change f to something like

    template <type_model... t_type_model>
    void f(container_model<container<t_type_model...>> auto) {}
    

    Summary

    Just to be clear, here is all the code with changes and this code fails to compile because the container_model constraints are not satisfied.

    #include <concepts>
    #include <string>
    #include <tuple>
    
    template <typename t>
    concept type_model = requires(t p_t) {
      typename t::inner;
    };
    
    template <typename t, typename u>
    concept same_inner = std::is_same_v<typename std::decay_t<t>::inner,
                                        typename std::decay_t<u>::inner>;
    
    template <typename t, typename... t_type_model>
    concept container_model = (same_inner<t, t_type_model> && ...);
    
    struct type_1 {
      using inner = int;
    };
    
    struct type_2 {
      using inner = std::string;
    };
    
    template <type_model... t_type_models> struct container {};
    
    template <type_model... t_type_model>
    void f(container_model<container<t_type_model...>> auto) {}
    
    using containers_X = container<type_1, type_2>;
    
    static_assert(not same_inner<type_1, type_2>);
    static_assert(not container_model<type_1, type_2>);
    
    int main() {
    
      containers_X _x;
    
      f(_x);
    
      return 0;
    }
    

    New Update

    The previous update was incomplete at best. Here is the latest code which I believe enforces the desired concepts.

    Sample Code

    #include <concepts>
    #include <string>
    #include <tuple>
    
    // Does T have an inner type named `inner`.
    template<class T>
    concept Inner = requires(T x) {
        typename T::inner;
    };
    
    // Do T and U have same inner type.
    template<class T, class U>
    concept SameInner = Inner<T> and Inner<U> and std::is_same_v<typename T::inner, typename U::inner>;
    
    // Helper for checking that all template parameters satisfy Inner
    // concept and all pairs satisfy SameInner.
    template<typename>
    struct inner_container_impl : std::false_type {};
    
    template<template<typename...> class Tp, Inner T>
    struct inner_container_impl<Tp<T>> {
        static constexpr bool value = true;
    };
    
    template<template<typename...> class Tp, Inner T, Inner... Ts>
    struct inner_container_impl<Tp<T, Ts...>> {
        static constexpr bool value = (SameInner<T, Ts> and ...);
    };
    
    // The concept just use the helper.
    template<class T>
    concept InnerContainer = inner_container_impl<T>::value;
    
    // Test types.
    struct type_1 {
        using inner = int;
    };
    
    struct type_2 {
        using inner = std::string;
    };
    
    struct type_3 {
        using inner = int;
    };
    
    template<Inner... Ts> struct container {};
    
    void f(InnerContainer auto) {}
    
    using containers_X = std::tuple<type_1, type_2>;
    using containers_Y = std::tuple<type_1, type_3>;
    
    static_assert(not SameInner<type_1, type_2>);
    static_assert(not InnerContainer<std::tuple<type_1, type_2>>);
    static_assert(not InnerContainer<containers_X>);
    
    int main() {
    
        containers_X _x;
        f(_x);
    
        containers_Y _y;
        f(_y);
    
      return 0;
    }
    

    Output

    /work/so/scratch/src/p4.cpp:64:5: error: no matching
          function for call to 'f'
        f(_x);
        ^
    /work/so/scratch/src/p4.cpp:52:6: note: candidate
          template ignored: constraints not satisfied [with auto:1 = std::tuple<type_1,
          type_2>]
    void f(InnerContainer auto) {}
         ^
    /work/so/scratch/src/p4.cpp:52:8: note: because
          'std::tuple<type_1, type_2>' does not satisfy 'InnerContainer'
    void f(InnerContainer auto) {}
           ^
    /work/so/scratch/src/p4.cpp:35:26: note: because
          'inner_container_impl<tuple<type_1, type_2> >::value' evaluated to false
    concept InnerContainer = inner_container_impl<T>::value;
                             ^
    1 error generated.