Reputation: 43
I would like to be able to do something like the following:
struct A {};
template<typename T, typename U> struct B : public A {};
std::unique_ptr<A> chooseB(int i, int j)
{
// return B<T, U> where T and U vary with i and j (0 = int, 1 = double etc.)
}
i
and j
are not known at compile time. Potentially the list of types could be long enough that just forming a big conditional that enumerates all possible pairs of i
and j
could be painful.
Is there are nice way to write the chooseB
function? I might potentially want to generalize this to more than two template parameters.
Upvotes: 1
Views: 280
Reputation: 137394
Make a jump table and dispatch.
template<size_t I, class... Ts>
std::unique_ptr<A> do_make() {
using T = std::tuple_element_t<I / sizeof...(Ts), std::tuple<Ts...>>;
using U = std::tuple_element_t<I % sizeof...(Ts), std::tuple<Ts...>>;
return std::make_unique<B<T,U>>();
}
template<class... Ts, size_t...Is>
std::unique_ptr<A> make(size_t i, size_t j, std::index_sequence<Is...>) {
using fptr_t = std::unique_ptr<A> (*)();
static constexpr fptr_t table[] = { do_make<Is, Ts...> ...};
return table[ i * sizeof...(Ts) + j]();
}
template<class... Ts>
std::unique_ptr<A> make(size_t i, size_t j) {
return make<Ts...>(i, j, std::make_index_sequence<sizeof...(Ts) * sizeof...(Ts)>());
}
Upvotes: 0
Reputation: 43
My pragmatic solution inspired by 0x499602D2 above. This will actually probably work for OK for the problem at hand - but I was interested in more general answers that don't rely on specific enumeration of all possibilities.
struct A
{
virtual ~A() = default;
virtual void printTypes() = 0;
};
template<typename T, typename U>
struct B : public A
{
virtual void printTypes() override
{
std::cout << typeid(T).name() << ", " << typeid(U).name() << std::endl;
}
};
int main()
{
std::map<std::pair<int, int>, std::function<std::unique_ptr<A> ()>> m =
{
{{0, 0}, [](){return std::unique_ptr<A>(new B<int, int>);}},
{{0, 1}, [](){return std::unique_ptr<A>(new B<int, double>);}},
{{1, 0}, [](){return std::unique_ptr<A>(new B<double, int>);}},
{{1, 1}, [](){return std::unique_ptr<A>(new B<double, double>);}}
};
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
auto a = m[std::make_pair(i, j)]();
a->printTypes();
}
}
}
Upvotes: 1
Reputation: 303870
Time for some abuse.
We're going to write a function that will figure out which type corresponds to a given index. But we can't return that type - the return type must be known at compile-time. So instead we'll forward that along to a function that we provide. I'll start with the end-result:
std::unique_ptr<A> chooseB(int i, int j)
{
using choices = typelist<int, double>;
return pick_elem<std::unique_ptr<A>>(i, choices{},
[=](auto tag1){
return pick_elem<std::unique_ptr<A>>(j, choices{},
[=](auto tag2) {
using T1 = typename decltype(tag1)::type;
using T2 = typename decltype(tag2)::type;
return std::make_unique<B<T1, T2>>();
});
});
}
pick_elem
takes three arguments: the index, the list of choices, and the function to forward along with. So we call it with the first index and call a lambda -- which in turns calls it with the second index and calls another lambda -- which finally makes our B
.
You have to explicitly provide the return type to pick_elem<>
because it needs to halt recursion somehow and that end-step will need to know what to return. So we start with some convenient metaprogramming helpers:
template <class... > struct typelist {};
template <class T> struct tag { using type = T; };
And then pick_elem
is:
template <class R, class F>
R pick_elem(int , typelist<>, F )
{
throw std::runtime_error("wtf");
}
template <class R, class T, class... Ts, class F>
R pick_elem(int i, typelist<T, Ts...>, F f)
{
if (i == 0) {
return f(tag<T>{});
}
else {
return pick_elem<R>(i-1, typelist<Ts...>{}, f);
}
}
This can be generalized to arbitrarily many types by making chooseB
variadic itself. In a way, this is actually cleaner than what I had before. chooseB()
now additionally takes some non-deducible arguments that are the types we've figured out so far:
template <class... Ts>
std::unique_ptr<A> chooseB()
{
return std::make_unique<B<Ts...>>();
}
template <class... Ts, class Idx, class... Rest>
std::unique_ptr<A> chooseB(Idx i, Rest... rest)
{
using choices = typelist<int, double>;
return pick_elem<std::unique_ptr<A>>(i, choices{},
[=](auto tag){
return chooseB<Ts..., typename decltype(tag)::type>(rest...);
});
}
You can add some safety around this by asserting that the number of arguments is correct.
Upvotes: 5
Reputation: 96845
You can map both numbers to instantiations of the base class:
#define FWD(x) std::forward<decltype(x)>((x))
auto p=[](auto&&l,auto&&r){return std::make_pair(FWD(l),FWD(r));};
std::unique_ptr<A> chooseB(int i, int j) {
std::map<std::pair<int,int>,std::unique_ptr<A>> m;
m.emplace(p(p(0,0),std::make_unique<B<int,int>>()));
m.emplace(p(p(0,1),std::make_unique<B<int,double>>()));
m.emplace(p(p(1,0),std::make_unique<B<double,int>>()));
m.emplace(p(p(1,1),std::make_unique<B<double,double>>()));
/* and so on for different (i,j) */
return std::move(m[p(i,j)]);
}
If you want to generalize this for more than two parameters, then have the function take a parameter pack (Args&&...
) possibly SFINAE-ified to all integers, and use a std::tuple<std::decay_t<Args...>>
instead of a pair.
Upvotes: 0