C++20 – how can I constrain template parameter pack arguments to a “chain” sequence e.g. F, F, F?

C++20 – how can I constrain template parameter pack arguments to a “chain” sequence e.g. F, F, F?


7

Suppose I have two classes:

template <typename X, typename Y> class Functor {};

template <typename Start, typename End, typename ... Functors> class Template {};

Template has the constraints:

  • All Functors must be type Functor

  • All Functor must be in a chain sequence, such that

    • the first Functor must have Start as its first argument
    • the last Functor must have End as its second argument
    • each Functor‘s first argument is the second argument of the Functor preceding it

    E.g. Functor<A,B>, Functor<B, C>, Functor<C, D>, ... etc.

Example:

Starting with: char

Ending with: long

Template<char, long, Functor<char, A>, Functor<A, B>, Functor<B, C>, Functor<C, long>> t;

                1         2         3         4
           |---------|---------|---------|---------|
argument: char       A         B         C        long
Functor #
  = 1      Functor<char, A>,
    2                Functor<A, B>,
    3                           Functor<B, C>,
    4                                    Functor<C, long>

Code

namespace ns
{
    template <typename X, typename Y = X>
    class Functor
    {
    public:
        using first  = X;
        using second = Y;

        Functor(X lVal) : x(lVal) {}

    private:
        X x;
    };

    template <typename Start, typename End, typename ... Functors>
        requires(std::is_convertible_v<Functors, Functor> && ...)    //error
    class Template
    {

        // How does one use `std::is_convertible_v` on
        // an un-specialized template class?
    };

    template <typename Start, typename End>
    class Template<Start, End, Functor<Start, End>>
    {};
}

Questions:

  1. What is the best approach?
    • Can this be done with fold expression(s)?
    • Or concepts?
  2. How does one use std::is_convertible (or any of the other metaprogramming traits) on an un-specialized template class?

If you’ve made it this far, thank you for your time, and thank you in advance for any light you can shed.

0

3 Answers
3


3

With (ab)use of operator overloading, you might do

// Used std::type_identity as wrapper
// Operator+, but no implementation needed
template <typename A, typename B, typename C>
std::type_identity<Functor<A, C>>
operator +(std::type_identity<Functor<A, B>>, std::type_identity<Functor<B, C>>);

template <typename A, typename B>
std::type_identity<A>
operator +(std::type_identity<Functor<A, B>>, std::type_identity<B>);

And then just check the operation is "valid".

template <typename Start, typename End, typename... Functors>
requires(std::is_same_v<std::type_identity<Start>,
                        decltype((std::type_identity<Functors>{} + ...)
                                 + std::type_identity<End>{})>)
class Template {
    //...
};

Demo

1

  • It seems that the second overload is not needed, just check whether the result type of (Functors + ...) is Functor<Start, End>, demo.

    – 康桓瑋

    3 hours ago


2

You could start by adding a type trait for checking that a template parameter is of Functor type:

template <typename X, typename Y>
class Functor {
   public:
    using first_type = X;
    using second_type = Y;
};

// type trait to check if a type is a Functor
template <class...>
struct is_Functor : std::false_type {};

template <class X, class Y>
struct is_Functor<Functor<X, Y>> : std::true_type {};

template <class T>
inline constexpr bool is_Functor_v = is_Functor<T>::value;

Then require that all of them are in fact of Functor type using a requires clause with a fold expression:

// Require Functors
template <typename Start, typename End, typename... Functors>
    requires(is_Functor_v<Functors> && ...)
class Template {

Then assert that the first Functor has Start as first parameter and that the last Functor has End as the second parameter:

    // helper type to get a Functor (type) at a specific index:
    template <std::size_t I>
    using funcat = typename std::tuple_element_t<I, std::tuple<Functors...>>;

    static_assert(std::is_same_v<
                  Start, typename funcat<0>::first_type>);

    static_assert(std::is_same_v<
                  End, typename funcat<sizeof...(Functors) - 1>::second_type>);

Then checking that the second parameter of each Functor is the same type as the first parameter of the next Functor using a lambda and fold expression:

    static_assert([]<std::size_t... I>(std::index_sequence<I...>) {
        return (... && std::is_same_v<typename funcat<I>::second_type,
                                      typename funcat<I + 1>::first_type>);
    }(std::make_index_sequence<sizeof...(Functors) - 1>()));

You could also make a concept out of the is_Functor type trait if you prefer this declaration:

template <class T>
concept functor = is_Functor_v<T>;

template <typename Start, typename End, functor... Functors>
class Template {
   //
};

Demo


As for the second question, "How does one use std::is_convertible (or any of the other metaprogramming traits) on an un-specialized template class?", you could again create a type trait that checks if a Functor can be instantiated by the supplied type without having to explicitly supply any template parameters.

Example:

template <class T>
struct is_convertible_to_Functor {
    static std::false_type test(...);

    template <class U = T>
    static auto test(U) -> decltype(Functor(std::declval<U>()), std::true_type{});

    static constexpr bool value = decltype(test(std::declval<T>()))::value;
};

template<class T>
concept convertible_to_Functor = is_convertible_to_Functor<T>::value;

And the class template would then need to use the Functor type each actual template parameter would be converted to:

template <typename Start, typename End, convertible_to_Functor... Functors>
class Template {
    template <std::size_t I>
    using funcat =
        std::tuple_element_t<I,
            std::tuple<decltype(Functor(std::declval<Functors>()))...>>;
    // converted to Functor     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    static_assert(std::is_same_v<Start, typename funcat<0>::first_type>);

    static_assert(std::is_same_v<
                  End, typename funcat<sizeof...(Functors) - 1>::second_type>);

    static_assert([]<std::size_t... I>(std::index_sequence<I...>) {
        return (... && std::is_same_v<typename funcat<I>::second_type,
                                      typename funcat<I + 1>::first_type>);
    }(std::make_index_sequence<sizeof...(Functors) - 1>()));
};

Demo where a std::pair<X, Y> is convertible into a Functor<X, Y> (odd conversion, but it’s just an example).


1

This answers the main question. The following seems to work. This gives the basic checking of proper sequence of Functors, and turning this into a concept, or adding a type traits-style using wrapper, etc…, can be left as a homework assignment:

class Start;
class End;

template <typename X, typename Y> class Functor {};

template<typename required, typename NextFunctor,
     typename ...RemainingFunctors> struct FunctorValidate;

template<typename required>
struct FunctorValidate<required, Functor<required, End>> {

    typedef void type_t;
};

template<typename required, typename NextFunctorType,
    typename FirstRemainingFunctor, typename ...RemainingFunctors>
struct FunctorValidate<required, Functor<required, NextFunctorType>,
    FirstRemainingFunctor, RemainingFunctors...>
    : FunctorValidate<NextFunctorType, FirstRemainingFunctor,
              RemainingFunctors...>
{
};

template<typename ...AllFunctors> struct FunctorChain
    : FunctorValidate<Start, AllFunctors...>
{
};

class A;
class B;
class C;

typedef FunctorChain<Functor<Start, A>,
             Functor<A, B>,
             Functor<B, C>,
             Functor<C, End>>::type_t ok1;

typedef FunctorChain<Functor<Start, End>>::type_t ok2;

#if 0
typedef FunctorChain<Functor<A, B>,
             Functor<B, C>,
             Functor<C, End>>::type_t error1;

typedef FunctorChain<Functor<Start, A>,
             Functor<A, B>,
             Functor<B, C>>::type_t error2;

typedef FunctorChain<Functor<Start, A>,
             Functor<B, C>,
             Functor<C, End>>::type_t error3;

#endif



Leave a Reply

Your email address will not be published. Required fields are marked *