Choose template based on run-time string in C++

2019-04-06 13:34发布

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.

6条回答
趁早两清
2楼-- · 2019-04-06 13:59

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");


}
查看更多
在下西门庆
3楼-- · 2019-04-06 14:03

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.

查看更多
Deceive 欺骗
4楼-- · 2019-04-06 14:04

You cannot do this. At best, you need to support a limited number of types, and switch between them using an if statement that can be evaluated at compile time.

查看更多
做自己的国王
5楼-- · 2019-04-06 14:04

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");
}
查看更多
乱世女痞
6楼-- · 2019-04-06 14:15

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.

查看更多
ら.Afraid
7楼-- · 2019-04-06 14:19
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

查看更多
登录 后发表回答