When doing template meta programming, IMO it always helps to keep the "meta part" and the "non meta part" separate for as long as possible. That way you can first think about the "meta part" as if it were a normal program, operating on types instead of values. So a template becomes a function, taking some type(s) (or for "higher order programming": other templates) as input and returns some type (or for "higher order programming": other templates).
So, first, step back for a second and don't think about templates, metaprogramming, and such. You have a list S
. For each item of S
, you want to call some function, and assemble a list of the returned items. So you need a function that, given one item of the list, returns the item it's mapped to. Let's call this function mapping
. You also need a function which takes said function and applies it to your list, i.e. calls the mapping
on each item and assembles the result list. Let's call that map
.
Now turn this into a meta program:
// mapping :: TYPE -> TYPE
// ---------------------------------------------------------
// ?? --> int (default "value")
template<typename X> struct mapping {
using type = int;
};
// if instead you want it to be undefined for unknown types:
//template<typename X> struct mapping;
// bool --> double
template<> struct mapping<bool> {
using type = double;
};
Now map
, generalized such that it can use anything like mapping
:
// map :: ([T] -> T) -> (T -> T) -> ([T] -> T)
// "List" "Mapping" result "type" (also a "List")
// --------------------------------------------------------
template<template<typename...> class List,
template<typename> class Mapping>
struct map {
template<typename... Elements>
using type = List<typename Mapping<Elements>::type...>;
};
Finally, apply to your Example
(which is kind of a list, because it "holds" multiple types) and the concrete mapping
:
template<typename... S>
using MappedExample = map<Example, mapping>::type<S...>;
Now you've got the resulting template, use it in your non-meta program:
template<typename... S>
MappedExample<S...> f() {
return MappedExample<S...>{};
}
Live example:
int main() {
std::cout
<< typeid(Example<bool,int,char,double>).name()
<< std::endl
<< typeid(decltype(f<bool, int, char, double>())).name()
<< std::endl;
}
Output:
7ExampleIJbicdEE
in the first line, means an Example
with template parameters bool, int, char, double.
7ExampleIJdiiiEE
as second line, means an Example
with template parameters double (mapped from the bool) and 3 int (the default mapping).