Converting std::type_identity object to a type

Viewed 276

Suppose that we create two type_of functions that return std::type_identity, like:

template<auto VAR>
auto type_of() {
    return std::type_identity<decltype(VAR)>{};
}

template<typename T>
auto type_of() {
    return std::type_identity<T>{};
}

The way to get an actual type from std::type_identity seems a bit cumbersome:

// this works
// both i1 and i2 are ints
decltype(type_of<int>())::type i1;
decltype(type_of<int{}>())::type i2;

Is there a way to waive the need for decltype in above expressions, or to put it inside a reusable expression, to achieve something nicer like:

// can this work?
type_of<int>()::type i1;
type_of<int{}>()::type i2;

Or even better:

// can this work??
type_of_t<int> i1;
type_of_t<int{}> i2;

Note: specialization for type and non-type template parameter, which could have been a direction, doesn't work (cannot compile):

template<auto>
struct type_of;

template<typename T>
struct type_of<T> { // <== compilation error: type/value mismatch 
    using type = T;
};

template<auto VAR>
struct type_of<VAR> {
    using type = decltype(VAR);
};
5 Answers

You can create a type alias. However you can't "overload" it. So my solution is to create two:

template <auto Var>
using type_of_var_t = decltype(type_of<Var>())::type;

template <class T>
using type_of_t = decltype(type_of<T>())::type;

auto test()
{
    type_of_var_t<11> i1 = 24;
    type_of_t<int> i2 = 17;

}

In C++, a template parameter must be a value, a type, or another template (which itself must fit within the declared template header). It must be exactly one of these.

If you want to do this:

the idea is to get something that is unaware on the caller side whether the template parameter is a type or a variable

The basic requirement for being able to do this is to write a template with a parameter that could be a value or a type.

That's not a thing C++ allows.

Template function overloading allows you to get away with something like that. But that only works because it isn't one template. It's two templates that are overloaded. Which one gets selected depends on the template arguments provided.

Template classes can't be overloaded. And template specialization cannot change the nature of the original template (like what its template parameters are). It can only allow you to reinterpret the template parameters of the original template parameters in order to provide an alternative implementation.

If you want this, you're going to have to wait until either C++ gets the ability to have a template parameter that could be anything or until C++ gets the ability to convert types into values and back (ie: reflection).

Getting the type from an std::type_identity object can be encapsulated into the following expresion:

template<auto x>
using type = typename decltype(x)::type;

This would allow to replace the cumbersome expressions:

decltype(type_of<int>())::type i1;
decltype(type_of<int{}>())::type i2;

With a more simple expression:

type<type_of<int>()> i1;
type<type_of<int{}>()> i2;

It still requires to go through two steps (type_of then type) as the first one shall be able to get a type or a variable, which is applicable only with function template overloading, then the function cannot return a type, so it returns an object that needs a template expression to extract the inner type.


Depending on what you want to do with the type, the code can become even simpler.

If all you want is to create an object of that type, you can forward the creation of the object into the function:

template<auto VAR, typename... Args>
auto create_type_of(Args&&... args) {
    return decltype(VAR){std::forward<Args>(args)...};
}

template<typename T, typename... Args>
auto create_type_of(Args&&... args) {
    return T{std::forward<Args>(args)...};
}

auto i1 = create_type_of<int>(7);
auto i2 = create_type_of<int{}>(7);

The generic case of creating a type from std::type_identity can work this way:

template<auto VAR>
constexpr auto type_of() {
    return std::type_identity<decltype(VAR)>{};
}

template<typename T>
constexpr auto type_of() {
    return std::type_identity<T>{};
}

template<typename T, typename... Args>
auto create(std::type_identity<T>, Args&&... args) {
    return T{std::forward<Args>(args)...};
}

auto i1 = create(type_of<int>(), 7);
auto i2 = create(type_of<int{}>(), 7);

Note that the entire thing works only with variables that can be used as non-type template parameters. For a more generic approach that works without templates (but with a macro...), and thus can work for variables that cannot be template parameters, see this mind blowing neat answer!

By design, template arguments in C++ templates are either templates, types or values.

Within the template, you know which they are. All expressions within the template that are dependent on the template arguments are disambiguated using typename or template keywords (or context) so their category is known before arguments are substituted.

Now there are metaprogramming libraries that work with value-substitutes for all 3.

template<class T> struct type_tag_t {using type=T;};
template<class T> constexpr type_tag_t<T> tag={};
template<template<class...>class Z> struct template_z_t {
  template<class...Ts>
  using result = Z<Ts...>;
  template<class...Ts>
  constexpr result<Ts...> operator()( type_tag_t<Ts>... ) const { return {}; }
};
template<template<class...>class Z>
constexpr template_z_t<Z> template_z = {};

here templates are mapped to constexpr function objects. The template_z template lets you map type-templates over to this domain.

template<auto x>
using type = typename decltype(x)::type;

template<auto x>
constexpr std::integral_constant<std::decay_t<decltype(x)>, x> k = {};

So,

constexpr auto vector_z = template_z<std::vector>;
constexpr auto vector_tag = vector_z( tag<int>, tag<std::allocator<int>> );

then you can go back to the type:

type<vector_tag> v{1,2,3,4};

this probably isn't what you want.

You might be willing to check the proposal for Universal Template Parameters

The implication example quite match your use-case of specializing for both TTP and NTTP :

template <template auto>
struct X;
template <typename T>
struct X<T> {
   // T is a type
   using type = T;
};
template <auto val>
struct X<val> : std::integral_constant<decltype(val), val> {
   // val is an NTTP
};
Related