Not a real meerkat
Not a real meerkat

Reputation: 5739

Robust method of getting the first template parameter

Assuming a template with a type-only list of template parameters, the following type trait can extract the first type argument:

template<typename T>
struct front {};

template<template<typename...> typename C, typename FirstT, typename... Args>
struct front<C<FirstT, Args...>> {using type = FirstT;};

template<typename T>
using front_t = typename front<T>::type;

template<typename...> struct foo{};

using std::is_same_v;
static_assert(is_same_v<front_t<foo<int, double>>, int>); // Ok
static_assert(is_same_v<front_t<foo<int, double>>, double>); // Fail (as expected)

This, however, doesn't work with templates that have value parameters:

using std::array;
static_assert(is_same_v<front_t<array<int, 5>>, int>);
// error: no type named 'type' in 'struct front<std::array<int, 5> >'

Ok, so now I need to take into account any value parameters too:

template<typename T>
struct front {};

// First parameter is a type, other parameters are types
template<template<typename...> typename C, typename FirstT, typename... Args>
struct front<C<FirstT, Args...>> {using type = FirstT;};

// First parameter is a type, other parameters are values
template<template<typename, auto...> typename C, typename FirstT, auto... Args>
struct front<C<FirstT, Args...>> {using type = FirstT;};

// First parameter is a value, other parameters are types
template<template<auto, typename...> typename C, auto FirstA, typename... Args>
struct front<C<FirstA, Args...>> {constexpr static const auto value = FirstA;};

// First parameter is a value, other parameters are values
template<template<auto...> typename C, auto FirstA, auto... Args>
struct front<C<FirstA, Args...>> {constexpr static const auto value = FirstA;};

// Avoid ambiguity if there's only a single type parameter
template<template<typename...> typename C, typename FirstT>
struct front<C<FirstT>> {using type = FirstT;};

// Avoid ambiguity if there's only a single value parameter
template<template<auto...> typename C, auto FirstA>
struct front<C<FirstA>> {constexpr static const auto value = FirstA;};

template<typename T>
using front_t = typename front<T>::type;

template<typename T>
const auto front_v = front<T>::value;

template<typename...> struct foo{};
template<auto...> struct bar{};

static_assert(std::is_same_v<front_t<foo<int>>, int>); // Ok
static_assert(std::is_same_v<front_t<foo<int, double>>, double>); // Fail (as expected)
static_assert(std::is_same_v<front_t<std::array<int, 5>>, int>); // Ok
static_assert(front_v<bar<5, 4>> == 5); // Ok
static_assert(front_v<bar<5, 4>> == 4); // Fail (as expected)

But then I try some more mixin':

template<typename, typename, auto...> struct baz{};

static_assert(std::is_same_v<front_t<baz<int, int>>, int>);
// error: no type named 'type' in 'struct front<baz<int, int> >'

This clearly is getting out of hand. Now, not only do I have to worry about mixing types with value parameters, I have to worry about the ordering of those parameters, and write a specialization for each combination of them. But all I want is the first of those parameters! The other ones shouldn't matter at all.

Finally, the question: Can I generically "ignore" any template parameters I don't really need for this type trait? By "generically" I mean to ignore both values and types.

Upvotes: 2

Views: 121

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275740

No.

As a general rule, using the way that arbitrary templates get their arguments as meaningful is a bad idea. This is just one of the many problems you'll run into.

Template arguments are positional, but there is no univerally agreed upon meaning for each position. Containers tend to have their first argument be the value type, but even that isn't true of associative containers (for whom the first two are synthesized into the value type).

template<class T>
using value_type = typename T::value_type;

that catches a bunch of cases. If we want to get the key type on assiciative containers, we do:

template<class T>
using key_type = typename T::key_type;

namespace details {
  template<class...>using void_t=void;
  template<template<class...>class Z, class, class...Ts>
  struct can_apply:std::false_type {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z,void_t<Z<Ts...>>, Ts...>:std::true_type {};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z,void,Ts...>;


template<template<class>class Z>
struct ztemplate1 {
  template<class T>
  using result = Z<T>;
};

template<bool b, template<class>class True, template<class> class False, class T>
using conditional_apply1 = typename std::conditional_t<
  b,
  ztemplate1<True>,
  ztemplate1<False>
>::template result<T>;

template<class X>
using container_first_type = conditional_apply1<
  can_apply<key_type, X>{},
  key_type,
  value_type,
  X
>;

and now container_first_type<std::map<std::string, int>> is std::string, while container_first_type<std::vector<int>> is int and container_first_type<std::array<double, 7>> is double.

Live example

Upvotes: 5

Jarod42
Jarod42

Reputation: 217810

There are no way to handle each cases.

What you can do as workaround:

  • provide typedef inside the class

    template <typename T /*, */> class Foo {
        using first_type = T;
    }
    
  • use only type, and possibly std::integral_constant

    Bar<int, std::true_type, std::integral_constant<int, 42>> bar;
    
  • Only handle only common cases as array.

Upvotes: 0

Related Questions