dexterurbane
dexterurbane

Reputation: 43

C++ conditional construction of template type

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

Answers (4)

T.C.
T.C.

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

dexterurbane
dexterurbane

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

Barry
Barry

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

David G
David G

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

Related Questions