Usually when you write a CLI tool which accepts parameter you have to deal with them. Most of the time you want to switch between behaviours based on the value of an argument.
The following is a common use case, where the program accepts a type and then prints something based on that type. I am using Boost to pre-process and auto generate the whole if-else
branches.
This is very nice in terms of maintainability as I only need to update a define
when I introduce a new type. On the other hand it is quite far from being modern and elegant.
I thought about using better-enums
to avoid using the if-else
to convert from string into an enum using the _from_string utility function. But then the way to go from enum to a type is still obscure to me.
Any suggestion on how to keep the nice maintainability of the current implementation but avoid to use pre-processor and macro functionalities?
#include <iostream>
#include <cstdlib>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/preprocessor/seq/for_each.hpp>
#include <type_traits>
using a_type = int;
using b_type = long;
using c_type = float;
using d_type = double;
#define TYPES (a)(b)(c)(d)
template<typename T>
void foo(){
T num = 1;
std::cout << typeid(decltype(num)).name() << " : "<< num << std::endl;
};
int main(int argc, char **argv)
{
if (argc < 1) {
return 1;
}
std::string type = argv[1];
if (false) {
#define LOOP_BODY(R, DATA, T) \
} \
else if (type == BOOST_PP_STRINGIZE(T)) { \
foo<BOOST_PP_CAT(T, _type)>(); \
BOOST_PP_SEQ_FOR_EACH(LOOP_BODY, _, TYPES);
#undef LOOP_BODY
} else {
std::cout << "ERROR: Unknown type " << type << std::endl;
}
}
Working example at https://wandbox.org/permlink/60bAwoqYxzU1EUdw
Another way is to use a plain array and std::find_if
instead of if-else
:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <typeinfo>
struct Handler {
char const* name;
void(*fn)(std::string const&); // Or std::function<> to accept lambdas.
};
struct A {};
struct B {};
template<class T>
void foo(std::string const& name) {
std::cout << "foo<" << typeid(T).name() << ">: " << name << '\n';
}
int main(int, char** av) {
Handler const handlers[] = {
{"a", foo<A>}
, {"b", foo<B>}
};
std::string const name = av[1];
auto handler = std::find_if(std::begin(handlers), std::end(handlers), [&name](auto const& h) {
return name == h.name;
});
if(handler != std::end(handlers))
handler->fn(name);
}
You don't need to use the preprocessor to store an arbitrary list of types and generate code for them. We can use variadic templates and compile-time strings. You can isolate preprocessor usage to the generation of pairs of names and types.
Firstly, let's define a wrapper for a compile-time sequence of characters. Note that the use of the _cs
literal is non-Standard, but available in every major compiler and likely to be part of C++20:
template <char... Cs>
using ct_str = std::integer_sequence<char, Cs...>;
template <typename T, T... Cs>
constexpr ct_str<Cs...> operator""_cs() { return {}; }
We can then define an empty type that stores a pair of a name and a type:
template <typename Name, typename T>
struct named_type
{
using name = Name;
using type = T;
};
And a macro to conveniently instantiate it:
#define NAMED_TYPE(type) \
named_type<decltype(#type ## _cs), type>
You can now use an empty variadic template class to store your types:
template <typename... Ts>
struct named_type_list { };
using my_types = named_type_list<
NAMED_TYPE(int),
NAMED_TYPE(long),
NAMED_TYPE(float),
NAMED_TYPE(double)
>;
Now, let's see how our main
should look:
int main()
{
const std::string input{"float"};
handle(my_types{}, input, [](auto t)
{
print(typename decltype(t)::name{});
});
}
The above will print out "float"
. We can implement handle
by unpacking the list of named_type
types and using a fold expression to find the matching type name:
template <typename... Ts, typename F>
void handle(named_type_list<Ts...>, const std::string& input, F&& f)
{
( (same(input, typename Ts::name{}) && (f(Ts{}), true) ) || ...);
}
Checking for equality between std::string
and ct_str
is annoying, but doable:
template <std::size_t... Is, char... Cs>
bool same_impl(const std::string& s,
std::integer_sequence<char, Cs...>,
std::index_sequence<Is...>)
{
return ((s[Is] == Cs) && ...);
}
template <char... Cs>
bool same(const std::string& s, std::integer_sequence<char, Cs...> seq)
{
return s.size() >= sizeof...(Cs)
&& same_impl(s, seq, std::make_index_sequence<sizeof...(Cs)>{});
}
final result live on wandbox.org
Note that this answer uses C++17 fold expressions. You can replace them in C++14 with one of the following techniques:
Recursive variadic template function, where the base case returns the default accumulation value, and the recursive case performs an operation between the tail and the head.
C++11 pack expansion tricks such as for_each_argument
.
The dispatching does short-circuit:
( (same(input, typename Ts::name{}) && (f(Ts{}), true) ) || ...);
This fold expression will stop at the first invocation of f
thanks to the , true
expression and the ||
operator.
empirical proof on wandbox.org