Handle variadic templates for general-case mixed types and non-types
Asked Answered
T

2

13

So I'm trying to make a type trait that says whether two "outer" class types are the same.

ie. std::vector<int> is the same as std::vector<double>, I don't care about any inner arguments for my type trait.

A problem that I had with trying to make a generic type trait for this is that I only know how to handle the typed variadic templates separately to the non-typed ones, which appears to stop me from making it generic.

Is it possible to handle any permutation of typed and non-typed template arguments?

Here's what I've implemented (including an example of where it fails):

// g++ -std=c++17

#include <iostream>
#include <vector>
#include <array>
#include <type_traits>

// If the outer types don't match
template <typename, typename>
struct is_outer_type_same : std::false_type {};


// if the arguments of the compared Type contains only types
// ie. std::vector<int,std::allocator<int>>
// (the inner arguments are also types)

template <template<typename...> typename OuterType,
                   typename... InnerTypes1,
                   typename... InnerTypes2
         >
struct is_outer_type_same < OuterType<InnerTypes1...>,
                            OuterType<InnerTypes2...>
                          >
        : std::true_type {};


// if the arguments of the compared Type contains only non-types
// eg. SomeClassName<4,5,2>

template <template<auto...> typename OuterType,
                   auto... InnerVariables1,
                   auto... InnerVariables2
         >
struct is_outer_type_same < OuterType<InnerVariables1...>,
                            OuterType<InnerVariables2...>
                          >
        : std::true_type {};


// if the arguments of the compared Type contains a single
// typed-argument followed by some non-typed arguments
// ie. std::array<int, 4>

template <template<typename, auto...> typename OuterType,
        typename InnerType1, auto... InnerVariables1,
        typename InnerType2, auto... InnerVariables2
         >
struct is_outer_type_same < OuterType<InnerType1, InnerVariables1...>,
                            OuterType<InnerType2, InnerVariables2...>
                          >
        : std::true_type {};


// For any outer types whose arguments have the pattern:
// single variable argument followed by a sequence of typed arguments:

template <template<auto, typename...> typename OuterType,
    auto InnerVariable1, typename... InnerTypes1,
    auto InnerVariable2, typename... InnerTypes2
         >
struct is_outer_type_same < OuterType<InnerVariable1, InnerTypes1...>, 
                            OuterType<InnerVariable2, InnerTypes2...>
                          >
        : std::true_type {};


// This is to make it neater to evaluate:

template <typename S, typename T>
inline constexpr bool is_outer_type_same_v
                    = is_outer_type_same<S,T>::value;


// Example type that fails to be handled
// correctly by the above struct templates: 
template <typename A, typename B, int C>
struct ExampleType
{
    A data1;
    B data2;
    const int data3 = C;
};


int main ()
{   
    // Examples to show where it fails:

    std::cout << "Fails to find match for ExampleType: "

    << is_outer_type_same_v<ExampleType<double,int,2>,
                            ExampleType<double,int,2>>

    << std::endl << std::endl



    // Examples to show where it works:

    << "Finds correctly: " << std::endl


    << std::endl << "Matches for std::vector: "

    << is_outer_type_same_v<std::vector<int>,
                            std::vector<double>>

    << is_outer_type_same_v<std::vector<std::vector<int>>,
                            std::vector<double>>

    << is_outer_type_same_v<std::vector<std::array<int,3>>,
                            std::vector<double>>


    << std::endl << "Mismatches for std::vector: "

    << is_outer_type_same_v<int,
                            std::vector<int>>

    << is_outer_type_same_v<std::array<int, 3>,
                            std::vector<int>>

    << is_outer_type_same_v<std::array<std::vector<int>, 3>,
                            std::vector<int>>


    << std::endl << "Matches for std::array: "

    << is_outer_type_same_v<std::array<int, 3>,
                            std::array<double, 7>>

    << is_outer_type_same_v<std::array<std::vector<int>, 7>,
                            std::array<double, 2>>

    << is_outer_type_same_v<std::array<std::array<int,3>, 8>,
                            std::array<std::vector<double>, 5>>


    << std::endl << "Mismatches for std::array: "

    << is_outer_type_same_v<int,
                            std::array<int,2>>

    << is_outer_type_same_v<std::vector<int>,
                            std::array<int,8>>

    << is_outer_type_same_v<std::vector<std::array<int,3>>,
                            std::array<int,2>>

    << std::endl;

    return 0;
}
Teniafuge answered 4/12, 2019 at 15:59 Comment(15)
AFAIK there is no generic solution for this. We either need reflection or some new type of template syntax that allows the parameter to be a type or non type parameter.Incomplete
This is indeed a limitation that the language currently does not have a work-around for. At least AFAIK. At the same time, mixing typed and untyped template parameters are fairly rare. Your version with auto... will work in most cases.Geometrize
@Geometrize I'm not sure I would call it rare. std::array is a major use case and there are plenty of classes out there like it for matrices and things of the like.Incomplete
@NathanOliver-ReinstateMonica And std::array styled things will work with the double-pack solution given here. It's not rare to mix, it rare to mix in the form of non-type, type, non-type, where as types.., non-types... is fairly common.Geometrize
@Geometrize Ah. Yeah, I have not seen anyone doing that.Incomplete
@Geometrize How would you handle the case of types..., non-types...? When I try to do that I always get errors. It’s not as generic as original question, but an answer to it would make my solution much better than what I have now...Teniafuge
@ElliottSmith I don't think even that can be done in a generic way. You would have to add specialization for type, non-types..., type, type, non-types... ect. Not very clean code, but once you stuff it away in your header you hpefully don't have to look at it very often. :-)Geometrize
@ElliottSmith Yes, that's what I mean when I say you need to add specializations. The non-type can be a pack, but the leading types would need one specialization each. type, non-types... and type, type, non-type... would be 2 specialization. If you add 10 you should have the behaviour you want with some arguably ugly code.Geometrize
@Geometrize Okay, Thanks for your help. It sounds like it’s not worth adding more specialisations without a specific need. It’s a shame to not have this functionality for library implementations...Teniafuge
Possible duplicate of Unify type and non-type template parametersCratch
@DavisHerring, Actually that question seems to combine my problem with the problem of passing incomplete data types as arguments, meaning each separate struct that handles the problem needs its own type/non-type permutation before specialization - hence that appears to be a much harder problem. Also the accepted answer is to the problem hasn't solved the questioner's original problem, nor has it solved mine. Also if they used my code they code solve it more neatly anyway, because I'm able to take advantage of c++17. What I'd like to know is if it's possible with modern c++...Teniafuge
@Elliott-ReinstateMonica: When your question is “Is it possible to…?”, an answer that shows as close as you can get is an answer. There may well be a better duplicate that uses (and shows the limitations of) auto template parameters.Cratch
Very interesting question, despite the answer seems to be "it's not possible". Fwiw, I think the question could be rephrased to "How do I pull the first template argument, being it a type or non-type, out of a type?". Well, pulling it out means putting it somewhere, and that somewhere can't be assigned indefferently a type and non-type.Rocco
@Enlico, Maybe I wasn't so clear, but the desired result should always be a type. I'm not interested n the first type/non-type of the list of args, instead i want to ignore the args entirely - I dont care about them in this case.Teniafuge
FYI, P2989 is currently under consideration. It proposes a universal template parameter that can be a type, non-type, or template name.Apothecary
T
0

Can you do it generically? No, not yet. But there are ways to do this (although they aren't the cleanest).

The first step would be to compare the inner "values". While struct template arguments don't do exactly what you want, function template arguments do (you can create "overloads" for typename/auto).

So with that knowledge, you can do something like this:

enum version {
    type, value
};

template <version...EE>
struct version_list {};

template <version...EE>
inline constexpr version_list<EE...> VL {};


template <template <class> class C, class T1>
constexpr auto decompose(C<T1>&)     { return VL<type>; }

template <template <auto> class C, auto V1>
constexpr auto decompose(C<V1>&)     { return VL<value>; }

template <template <class, class> class C, class T1, class T2>
constexpr auto decompose(C<T1, T2>&) { return VL<type, type>; }

template <template <class, auto> class C, class T1, auto V1>
constexpr auto decompose(C<T1, V1>&) { return VL<type, value>; }

/* continue repeating for different variations... */

This will allow you to compare the amount/type of template arguments like so:

template <typename T>
using decompose_t = decltype(decompose(std::declval<T&>()));

template <version...EE1, version...EE2>
constexpr bool compare_versions(
    const version_list<EE1...>, 
    const version_list<EE2...>) 
{
    if constexpr(sizeof...(EE1) != sizeof...(EE2)) return false;
    else return ((EE1 == EE2) && ...);
}

template <typename T, typename U>
inline constexpr bool compare_versions_v 
     = compare_versions(decompose_t<T>{}, decompose_t<U>{});

Finally, you can create your comparison like so:

template <typename T, typename U>
constexpr bool is_outer_type_same() {
    if(compare_versions_v<T, U>) {
        /* Comparison logic */
    }
    else return false;
}

template <typename T, typename U>
inline constexpr bool is_outer_type_same_v = is_outer_type_same<T, U>();

Where you don't even attempt to check if the outer types match if they don't have the same template parameters.

I left out the actual comparison logic because it's very similar to what you would be doing in the first check. You would probably want to write some code to generate all these functions and write them to a file, which you can then include/import into the code actually being exposed.

But all of this could be avoided if we had typename auto :P

Trolley answered 15/3, 2023 at 0:28 Comment(0)
R
0

If you want something that just works, this could be one approach. It's a bit specific and hacky, but works for this scenario:

#include <string_view>

template <typename T>
constexpr std::string_view type_signature() {
#ifdef _MSC_VER
    return __FUNCSIG__;
#else
    return __PRETTY_FUNCTION__;
#endif
}

template <typename T1, typename T2>
constexpr bool is_outer_type_same() {
    auto marker_sig = type_signature<int>();

    auto t1_sig = type_signature<T1>();
    auto t2_sig = type_signature<T2>();

    std::size_t type_start_pos = marker_sig.find("int");

    auto pos1 = t1_sig.find('<', type_start_pos);
    auto pos2 = t2_sig.find('<', type_start_pos);

    return t1_sig.substr(type_start_pos, pos1 - type_start_pos) == t2_sig.substr(type_start_pos, pos2 - type_start_pos);
}
Rig answered 29/4, 2024 at 13:4 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.