Intro
In my codebases, there’re lots of situations where different operations are dispatched based on data types that are undetermined until runtime. So one problem here is how to do the mapping. Consider the following code snippet,
template<typename T>
bool foo(const int& v) {
std::cout << __PRETTY_FUNCTION__ << " " << v << "\n";
return sizeof(T) > 4;
}
foo
is a function template whose template parameter can’t be deduced here. It’s just a showcase and doesn’t have specific meanings. Now, we want to call it of types according to a value from the runtime, networking, or files. Say I use enums here,
enum class DataType: uint8_t {
kInt32,
kDouble,
kString
};
The enumerator names represent types being mapped to.
Basic Ideas
When I want to dispatch it, I might do this,
auto foo_wrapper(DataType type, int& arg) {
switch (type) {
case DataType::kInt32:
return foo<int32_t>(arg);
case DataType::kDouble:
return foo<double>(arg);
case DataType::kString:
return foo<std::string>(arg);
default:
return false;
}
}
This may be the roughest idea at the first glance. However, whenever I have a function template, I have to duplicate this bunch of code, which is unacceptable…
Is there any way to extract this code to a more general case? You may say template templates and pass function templates. Unfortunately, it’s not allowed.
But wrapping a function into a struct and passing it as a template template argument is valid. Yes, LAMBDAs!
Lambda
template <typename F>
auto dispatcher(F&& f, DataType t) {
switch (t) {
case DataType::kInt32:
return f(int32_t{});
case DataType::kDouble:
return f(double{});
case DataType::kString:
return f(std::string{});
default:
return false;
};
}
auto foo_wrapper(DataType type, int& arg) {
return dispatcher([&](auto t) { return foo<decltype(t)>(arg); }, type);
}
The main idea is to wrap the function template into a generic lambda, whose parameter type is the mapped one. In the dispatching part, we construct a default value of its type so that it can be deduced. Godbolt
templated lambda introduced in C++20 will make it neater. You don’t have to pass something to be deduced.
template <typename F>
auto dispatcher(F&& f, DataType t) {
switch (t) {
case DataType::kInt32:
return f.template operator()<int32_t>();
case DataType::kDouble:
return f.template operator()<double>();
case DataType::kString:
return f.template operator()<std::string>();
default:
return false;
};
}
auto foo_wrapper(DataType type, int& arg) {
return dispatcher([&]<typename T>() { return foo<T>(arg); }, type);
}
std::visit
In C++17, std::visit
already provides a way for you to dispatch. See the following solution,
using VType = std::variant<std::type_identity<int32_t>,
std::type_identity<double>,
std::type_identity<std::string>>;
static const std::unordered_map<DataType, VType> dispatcher = {
{DataType::kInt32, std::type_identity<int32_t>{}},
{DataType::kDouble, std::type_identity<double>{}},
{DataType::kString, std::type_identity<std::string>{}}
};
auto foo_wrapper(DataType type, int arg1)
{
return std::visit([&](auto v){
return foo<typename decltype(v)::type>(arg1);
}, dispatcher.at(type));
}
std::type_identity
is introduced in C++20, but you can implement one by yourself. I believe this solution is a bit more elegant because it avoids the branch selection code.
However, the overhead of std::visit
is non-negligent here. If you care about runtime performance, you’d better take care.
Summary
In this post, I provide a few “elegant” ways to map runtime values to types. Since the boilerplate code is necessary, I can’t say it’s 100% elegant. If you also have some good solutions or suggestions, feel free to leave a comment or contact me.