Choose template based on run-time string in C++
Asked Answered
G

5

12

I have an attribute vector that can hold different types:

class base_attribute_vector; // no template args

template<typename T>
class raw_attribute_vector : public base_attribute_vector;

raw_attribute_vector<int> foo;
raw_attribute_vector<std::string> foo;

Based on run-time input for the type, I would like to create the appropriate data structure. Pseudocode:

std::string type("int");
raw_attribute_vector<type> foo;

Obviously, this fails. An easy, but ugly and unmaintainable workaround is a run-time switch/chained if:

base_attribute_vector *foo;
if(type == "int") foo = new raw_attribute_vector<int>;
else if(type == "string") ...

I read about run-time polymorphism with functors, but found it quite complex for a task that is conceptually easy.

What is the best and cleanest way to make this work? I played around with boost::hana, finding that while I can create a mapping from string to type, the lookup can only be done at compile time:

auto types = 
hana::make_map(
    hana::make_pair(BOOST_HANA_STRING("int"), hana::type_c<int>),
    hana::make_pair(BOOST_HANA_STRING("string"), hana::type_c<std::string>)
);

All possible types are known at compile-time. Any suggestions are highly appreciated. In a perfect solution, I would create the name->type mapping in a single place. Afterwards, I would use it like this

std::vector<base_attribute_vector*> foo;

foo.push_back(magic::make_templated<raw_attribute_vector, "int">);
foo.push_back(magic::make_templated<raw_attribute_vector, "std::string">);

foo[0]->insert(123);
foo[1]->insert("bla");

foo[0]->print();
foo[1]->print();

It is not required for this magic to happen at compile time. My goal is to have as readable code as possible.

Gurgle answered 28/7, 2016 at 18:28 Comment(10)
Maybe I'm missing something, but any template instantiation happens by definition at compiletime, so I'm not sure what you expect. You can of corse let all templates inherit from a common base class and then employ run-time polymorphism, but the range of instantiated templates is still defined at compiletime.Cherianne
Let's suppose we've solved it and you are able to create the appropriate data structure at runtime. Can you show us an example of how you are going to use it? Chances are high that if you can construct that example, you will also be able to solve the stated problem.Decorum
@Cherianne Yes, that's the idea. I edited the question to hopefully make it a bit clearer.Gurgle
@Decorum I added an example at the end how I wish it could look like.Gurgle
Your comments make me feel more and more that this is a solved problem and I just don't know the right way to ask my question or what to put into Google. ;)Gurgle
"conceptually easy" - C++ is not a dynamically typed language (like, e.g., Python). You need static typing throughout. Now that you have an example of the declaration of this container (per @Leon) now add two more examples: usages of this container as it holds two different types. The key problem is type-safe access. To see what I'm talking about, look at an existing type such as boost::variant, especially at its accessors (hint: it uses the visitor pattern). In fact ... maybe you should just use boost::variant.Oxbridge
Hmm... something like that might work if you had a LiteralType string class, so you could construct some constexpr instances of it and use those. That seems overly complex for this, though, and/or not what you want.Gimbals
@Oxbridge added usage in the example. I am already using boost::variant in a different place. If I understand you correctly, you are suggesting a raw_attribute_vector<boost::variant<...>>. This is an issue because to my understanding, the variant is as big as the biggest potential value. This would break the performance of the attribute vector.Gurgle
Your raw_attribute_vector<T> should contain a member of type boost::variant<std::vector<T>,std::vector<U>,...>, or alternatively, std::vector<boost::variant<T,U,V...>>, depending on whether or not the attribute vectors are homogenous in some type. In the first case the boost::variant is always holding some kind of std::vector - the size for which is always the same fixed small amount no matter what type it is instantiated at, in the second case it is directly holding a std::vector which, likewise, has a fixed small size. Because: The vector holds its elements in the heap.Oxbridge
Alternatively, you could use boost::variants which hold std::unique_ptrs to the attribute value. In other words, store all attributes on the heap.Oxbridge
U
8
enum class Type
{
    Int,
    String,
    // ...
    Unknown
};

Type TypeFromString(const std::string& s)
{
    if (s == "int") { return Type::Int; }
    if (s == "string") { return Type::String; }
    // ...
    return Type::Unknown;
}

template <template <typename> class>
struct base_of;

template <template <typename> class C>
using base_of_t = typename base_of<C>::type;

And then the generic factory

template <template <typename> class C>
std::unique_ptr<base_of_t<C>> make_templated(const std::string& typeStr)
{
    Type type = TypeFromString(typeStr);
    static const std::map<Type, std::function<std::unique_ptr<base_of_t<C>>()>> factory{
        {Type::Int, [] { return std::make_unique<C<int>>(); } },
        {Type::String, [] { return std::make_unique<C<std::string>>(); } },
        // ...
        {Type::Unknown, [] { return nullptr; } }
    };
    return factory.at(type)();
}

a specialization is needed for each base:

template <>
struct base_of<raw_attribute_vector> {
    using type = base_attribute_vector;
};

And then

auto p = make_templated<raw_attribute_vector>(s);

Demo

Unmannered answered 28/7, 2016 at 19:13 Comment(4)
linear time lookup makes me sad :(Adlib
The nested template of make_templated was the idea I was missing. I think I won't need the automatic deduction of the base class and will leave that out so that I don't need the specialization (see my answer). Thanks a lot!Gurgle
@RichardHodges: I think one could also add the map idea from Guillaume to get around the linear time lookup (at the cost of readability). In our case, I believe this should not cause a performance issue.Gurgle
@RichardHodges Use a prefilled map to hold string/enum type pairs for lookup. That is O(1).Scratches
K
11

I'd use an std::map that has strings as key and std::function as values. I would associate the string with a function that returns your type. Here's an example:

using functionType = std::function<std::unique_ptr<base_attribute_vector>()>;
std::map<std::string, functionType> theMap;

theMap.emplace("int", []{ return new raw_attribute_vector<int>; });
theMap.emplace("float", []{ return new raw_attribute_vector<float>; });

// Using the map
auto base_vec = theMap["int"](); // base_vec is an instance of raw_attribute_vector<int>

Of course, this solution is valid if you only know the string value at runtime.

Kristelkristen answered 28/7, 2016 at 18:55 Comment(2)
Great answer - I like how short it is. What I like better about the answer that I accepted is that I can reuse the same code in different places without having to create a new map.Gurgle
@Gurgle You can change the accepted answer. I also agree that this is the more elegant solution.Stole
U
8
enum class Type
{
    Int,
    String,
    // ...
    Unknown
};

Type TypeFromString(const std::string& s)
{
    if (s == "int") { return Type::Int; }
    if (s == "string") { return Type::String; }
    // ...
    return Type::Unknown;
}

template <template <typename> class>
struct base_of;

template <template <typename> class C>
using base_of_t = typename base_of<C>::type;

And then the generic factory

template <template <typename> class C>
std::unique_ptr<base_of_t<C>> make_templated(const std::string& typeStr)
{
    Type type = TypeFromString(typeStr);
    static const std::map<Type, std::function<std::unique_ptr<base_of_t<C>>()>> factory{
        {Type::Int, [] { return std::make_unique<C<int>>(); } },
        {Type::String, [] { return std::make_unique<C<std::string>>(); } },
        // ...
        {Type::Unknown, [] { return nullptr; } }
    };
    return factory.at(type)();
}

a specialization is needed for each base:

template <>
struct base_of<raw_attribute_vector> {
    using type = base_attribute_vector;
};

And then

auto p = make_templated<raw_attribute_vector>(s);

Demo

Unmannered answered 28/7, 2016 at 19:13 Comment(4)
linear time lookup makes me sad :(Adlib
The nested template of make_templated was the idea I was missing. I think I won't need the automatic deduction of the base class and will leave that out so that I don't need the specialization (see my answer). Thanks a lot!Gurgle
@RichardHodges: I think one could also add the map idea from Guillaume to get around the linear time lookup (at the cost of readability). In our case, I believe this should not cause a performance issue.Gurgle
@RichardHodges Use a prefilled map to hold string/enum type pairs for lookup. That is O(1).Scratches
A
1

I'd probably do something like this:

Features:

  • 1 - time registration of objects by passing a named prototype

  • constant time lookup at runtime

  • lookup by any type which can be compared to std::string

-

#include <unordered_map>
#include <string>


struct base_attribute_vector { virtual ~base_attribute_vector() = default; };

template<class Type> struct attribute_vector : base_attribute_vector {};

// copyable singleton makes handling a breeze    
struct vector_factory
{
    using ptr_type = std::unique_ptr<base_attribute_vector>;

    template<class T>
    vector_factory add(std::string name, T)
    {
        get_impl()._generators.emplace(std::move(name),
                                       []() -> ptr_type
                                       {
                                           return std::make_unique< attribute_vector<T> >();
                                       });
        return *this;

    }

    template<class StringLike>
    ptr_type create(StringLike&& s) const {
        return get_impl()._generators.at(s)();
    }

private:
    using generator_type = std::function<ptr_type()>;

    struct impl
    {
        std::unordered_map<std::string, generator_type, std::hash<std::string>, std::equal_to<>> _generators;
    };


private:

    static impl& get_impl() {
        static impl _ {};
        return _;
    }

};



// one-time registration

static const auto factory =
vector_factory()
.add("int", int())
.add("double", double())
.add("string", std::string());


int main()
{
    auto v = factory.create("int");
    auto is = vector_factory().create("int");

    auto strs = vector_factory().create("string");


}
Adlib answered 28/7, 2016 at 19:19 Comment(0)
G
1

Largely based on Jarod42's answer, this is what I will be using:

class base_attribute_vector {};

template<typename T>
class raw_attribute_vector : public base_attribute_vector {
public:
raw_attribute_vector() {std::cout << typeid(T).name() << std::endl; }
};

template<class base, template <typename> class impl>
base* magic(std::string type) {
    if(type == "int") return new impl<int>();
    else if(type == "float") return new impl<float>();
}

int main() {
    auto x = magic<base_attribute_vector, raw_attribute_vector>("int");
    auto y = magic<base_attribute_vector, raw_attribute_vector>("float");
}
Gurgle answered 28/7, 2016 at 19:23 Comment(0)
I
-1

Short answer: no, you can't instruct the compiler to evaluate a runtime condition in compile time. Not even with hana.

Long answer: there are some (mostly language independent) patterns for this.

I'm assuming that your base_attribute_vector has some virtual method, most likely pure, commonly called an interface in other languages.

Which means that depending on the complexity of your real problem, you probably want a factory or an abstract factory.

You could create a factory or abstract factory without virtual methods in C++, and you could use hana for that. But the question is: is the added complexity really worth it for that (possibly really minor) performance gain?

(also if you want to eliminate every virtual call, even from base_attribute_vector, you have to make everything using that class a template, after the entry point where the switch happens)

I mean, have you implemented this with virtual methods, and measured that the cost of the virtual calls is too significant?

Edit: another, but different solution could be using a variant type with visitors, like eggs::variant.

With variant, you can create classes with functions for each parameter type, and the apply method will switch which function to run based on it's runtime type.

Something like:

struct handler {
  void operator()(TypeA const&)  { ... }
  void operator()(TypeB const&)  { ... }
  // ...
};

eggs::variant< ... > v;
eggs::variants::apply(handler{}, v);

You can even use templated operators (possibly with enable_if/sfinae), if they have common parts.

Incomprehensive answered 28/7, 2016 at 18:46 Comment(2)
Maybe I'm understanding the factory pattern wrong here. Wouldn't it require the same switch based on the string? Also, wouldn't it be limited to creating raw_attribute_vectors and need a second factory for when I want to create other objects based on the type? Evaluation at compile-time is not needed - I'm trying to get code that hides most of the datatype-uglyness from places where it should not make a difference.Gurgle
Yes, you have to create a switch/map/something somewhere. The factory just encapsulates this - also I edited my answer with a variant idea, it's a bit different, but might be what you are looking for.Incomprehensive

© 2022 - 2024 — McMap. All rights reserved.