Home > Net >  How can I build a constexpr vector of types which can append types?
How can I build a constexpr vector of types which can append types?

Time:10-11

My colleague challenged me to build a TypeVector, which is compile time vector-like container which can push_back types and delete types at compile time. Given a constexpr TypeVector you should be able to query back the actual type from an element of the TypeVector.

PSEUDOCODE EXAMPLE USAGE OF TypeVector:

consteval auto build_vector() {
   TypeVector<2> x; //TypeVector of size 2
   x.push_back(int);
   x.push_back(float);
   return x;
}

static constexpr auto my_vector = build_vector();
// Get type from first element of my_vector 
typename retrieve_type<my_vector.get<0>()>::type some_variable = 0; // equivalent to int some_variable = 0; 

// Get type from second element of my_vector 
typename retrieve_type<my_vector.get<1>()>::type some_variable2 = 0.0f; // equivalent to float some_variable2 = 0.0f; 

I think the best way to do this is to use std::array The problem is that std::array cannot store types but needs to store values of the same type, so I am transforming types into values like so:

template <typename T>
struct TypeHash{
   static constexpr char obj; // Create one obj per type
   static constexpr char* value = &obj; // This will be different for each type
   
};
template <typename T>
static constexpr char* to_value = TypeHash<T>::value;

// Now I can do to_value<int> and to_value<float> to "store" the float and int as values of type const char*
// Example usage:

template <int N>
using TypeVector<N> = std::array<const char*, N>;
consteval auto some_func() {
   TypeVector<2> x;
   x[0] = to_value<int>;
   x[1] = to_value<float>;
   return x;
}

static constexpr auto first_type = some_func()[0]; 

The problem is reversing this operation: creating retrieve_type from the pseudocode.

I thought of creatingretrieve_type as a const char* to type map through template specialisations like so:

static constexpr auto first_type = some_func()[0]; // From above 

template < const char* value> struct retrieve_type {};
template <> struct retrieve_type<to_value<int>> { using type = int; };
template <> struct retrieve_type<to_value<float>> { using type = float; };


// Now I can get the actual type from first type:
typename retrieve_type<first_type>::type y = 0; // int x = 0;


There are a few problems with this approach:

  1. I have to create a retrieve_type specialisation for each existing type ( not a huge problem, I can handle this, but it is not ideal)

  2. (Bigger problem) How can I create retrieve_type specialisations for templates??

template <typename T>
struct SomeTemplateClass {};
// How to create a specialisation of retrieve_type for all instantiations of SomeTemplateClass?

How can I make this work for types instantiated from a template? Can I avoid manually creating a retrieve_type specialisation for each and every type?

I am open to using any third party libraries / any compiler specific options.

CodePudding user response:

C is a statically typed language. This means that, at compile time, the type of every expression must be known. It also means that the fundamental properties of every type are known at compile-time. Those properties are also well-defined at the point of the type's definition and cannot be changed.

If you have some construct that stores types, and you want to extract a type from it such that you can do normal type-stuff with that "value" (declare variables of that type, etc), then you must do it in a way that works within the limitations of a statically typed language.

This means that the construct that stores those types must be a type. Not an object, an actual type. There is no getting around that fact: if you want to be able to do type-computations, in order for the result of that operation to be a type, the source of that operation must also be a type (constexpr functions get to play games where you can pass "types" around as parameter values through a wrapper object, but that's just a trick of template argument deduction; the primary computation itself is on types).

This means that a type, in this paradigm, is a value. But since C is statically typed, such types are immutable. Whatever properties are on a type at the point of that type's definition are the properties that exist on it at all points in the program.

Always.

As such, we have to treat type-values in a functional programming way. In particular, type-values are immutable "objects". Whatever value they have is the value they have.

It's like the number 2. You can increment the number 2, but this doesn't change "2". It creates a new number 3, which is different from 2. "2" still exists even after you incremented it.

You must treat type-values in the same way. If you have a type-value that represents a list of types, you cannot append to it. You can create a new type-value which contains the previous list of types plus the one you appended. But the old type-value list still "exists".

There is no other way in C to have "values" that represent types that can be extracted in some way. At least, not until C gets some form of reflection.

Your attempt to get around this with "hashes" is clever, but it simply moves the immutable data.

Instead of having an immutable list of types, you have an immutable registry of types. This registry must be finite. It also maps between numbers and types. And templates are not types; they are meta-functions which generate types. std::vector isn't a type; std::vector<int> is a type generated by the std::vector metafunction.

A mapping function between numbers and types cannot map to between numbers and type generation meta-functions. C , being a statically typed language, doesn't allow that.


Your attempt to get around immutability is clever, but it's still immutable. You traded immutability of lists for immutability of the table that maps between numbers and types.

But this list is also something else: finite.

In C , a template is a meta-function that generates stuff: functions, variables, or types. A "template type" isn't really a thing. std::vector isn't a type; it is a thing which generates types based on a set of parameters. std::vector<int> is a type, generated by the std::vector meta-function.

If you have a mapping between numbers and types, then it maps between numbers and types, not type-metafunctions. Just like if you have a function parameter that takes an int, that function cannot be made to take a function that returns an int (you could create a new function that does this, but nevermind that now).

As such, you cannot store "templates" in your mapping table. Or rather, you could either store templates with a specific parameter list, or store things that aren't templates. But you can't do both.


There are dozens of such libraries available in C . Boost.Hana has type-list functionality, as do many other libraries. But they're all going to have the limitations of C .

But you can do basic type retrieval on a std::tuple. std::tuple_element_t<I, tpl> gets the type of the Ith type in tpl, and std::tuple_size_v<tpl> computes the number of elements in tpl. And basic appending functionality isn't hard either:

template<typename Tpl, typename T>
struct append_tuple; //No definition.

template<typename ...Types, typename T>
struct append_tuple<std::tuple<Types...>, T>
{
  using type = std::tuple<Types..., T>;
};

template<typename Tpl, typename T>
using append_tuple_t = typename append_tuple<Tpl, T>::type;

And no, you're not going to be able to (easily) map a string name to a type.

CodePudding user response:

Templates can be bijecticely mapped to types.

template<class T>
strict tag_t{using type=T;};
template<class T>
constexpr tag_t<T> tag={};

template<template<class...>class Z>
struct ztemplate{
  template<class...Ys>
  constexpr auto operator()(tag_t<Ys>...)const{
    return tag<Z<Ys...>>;
  }
};

This handles one category of templates.

If you solve the value to type map problem, you can solve the value to template problem. And with the above a template is just a stateless constexpr function object on type tags.

For non type template parameters, you need a different tag type and different stateless function.objects. For example, a template that takes an std integral constant type can wrap a template that takes an integer value, making it better behaved.

As for value to type, that is just a long slow verbose slog.

You can build a parse tree in a flat list of integers.

Each node has a value and a count of children, both constexpr. You grab the first value and map via tag dispatching to a tag type. If the number of elements is more than 1 then invoke the tag dispatched result with recursively evaluated type tags generated from the list of integers.

template<std::size_t I>
using index_t=std::integral_constant<std::size_t,I>;
template<std::size_t I>
constexpr index_t<I> index{};    
constexpr tag_t<int> map_to_type(index_t<0>){return {};}
constexpr ztemplate<std::vector> map_to_type(index_t<1>){return {};}
constexpr auto make_type(){
  constexpr int arr[]={1,0};
  return map_to_type(index<arr[0]>)(map_to_type(index<arr[1]>));
}
constexpr auto tag=make_type();
decltype(tag)::type vec={1,2,3};
  • Related