2019-04-16

Dynamic dispatching reloaded

In previous posts I was exploring a feature I am interested in, and which I have called in many different ways — here I will call it dynamic dispatching.

The previous articles:

Conclusion of those posts: out of the box only Perl6 has it.

The story went on like this: I stirred the net for a solution in C++. And I obtained it.

I asked on Reddit and the user alfps gave me a solution.

Unfortunately it needs a C++17 compilers. Indeed, I was able to change it so that now a C++14 compliant compiler suffices. Also, I've removed the usage of the syntax auto f() -> Type since the more common Type f() is good for me.

I haven't analyzed it yet to see how it really works and to check if it can fit my “needs”, but anyway running it gives the expected results:

sum(S,S)
sum(S,I)
sum(I,S)
sum(I,I)
!oops - operator() - No function corresponds to the argument types

The code:

// see https://www.reddit.com/r/cpp_questions/comments/afp4ae/dynamic_argumentbased_dispatch_will_it_ever_be/eeh8qe7/
// credits to https://www.reddit.com/user/alfps
// I've modified it so that it can be compiled with a C++14 compliant compiler, plus minor style
// modifications, e.g., I've changed the syntax auto x()->ReturnValue in function declaration and defines,
// since there's no point in using it.
#include <array> // std::array
#include <functional> // std::function
#include <iostream>
#include <map> // std::map
#include <stdexcept> // std::exception
#include <string> // std::string
#include <typeindex> // std::type_index
#include <type_traits> // std::is_polymorphic
#include <utility> // std::move
#define $fail(s) cppx::fail(std::string() + __func__ + " - " + s)
namespace std
{
// this was added in C++17
template<class T>
constexpr bool is_polymorphic_v = is_polymorphic<T>::value;
}
namespace cppx
{
using std::runtime_error;
using std::string;
template<class Type> using P_ = Type*;
bool hopefully(const bool condition)
{
return condition;
}
bool fail(const string& message)
{
throw runtime_error(message);
}
} // namespace cppx
namespace app
{
using cppx::P_;
using cppx::hopefully;
using cppx::fail;
using std::cout;
using std::endl;
using std::array;
using std::function;
using std::map;
using std::type_index;
using std::is_polymorphic_v;
using std::move;
void say(const P_<const char> s)
{
cout << s << endl;
}
struct Base
{
virtual ~Base() {}
};
struct String : Base {};
struct Integer : Base {};
double sum_ss(P_<const String>, P_<const String>) { say("sum(S,S)"); return 1; }
double sum_si(P_<const String>, P_<const Integer>) { say("sum(S,I)"); return 2; }
double sum_is(P_<const Integer>, P_<const String>) { say("sum(I,S)"); return 3; }
double sum_ii(P_<const Integer>, P_<const Integer>) { say("sum(I,I)"); return 4; }
class Dispatcher
{
using Func = double (P_<const void>, P_<const void>);
using Args_id = array<type_index, 2>;
using Args_to_func = map<Args_id, function<Func>>;
Args_to_func m_functions;
public:
template<class Arg_1, class Arg_2>
double operator()(P_<const Arg_1> a1, P_<const Arg_2> a2) const
{
static_assert(is_polymorphic_v<Arg_1>);
static_assert(is_polymorphic_v<Arg_2>);
const auto it = m_functions.find({typeid(*a1), typeid(*a2)});
hopefully(it != m_functions.end())
or $fail( "No function corresponds to the argument types" );
return it->second(a1, a2);
}
template<class Arg_1, class Arg_2>
void add_wrapped(function<double(P_<const Arg_1>, P_<const Arg_2>)> f)
{
static_assert(is_polymorphic_v<Arg_1>);
static_assert(is_polymorphic_v<Arg_2>);
const Args_id args_id = {typeid(Arg_1), typeid(Arg_2)};
hopefully(m_functions.count(args_id) == 0)
or $fail( "That function signature has already been added." );
const auto anon_f =
[f](P_<const void> a1, P_<const void> a2) -> double
{
return f(static_cast<P_<const Arg_1>>(a1), static_cast<P_<const Arg_2>>(a2));
};
m_functions.insert({ args_id, anon_f });
}
// Just an argument type deduction convenience for adding free-standing functions.
template<class Arg_1, class Arg_2>
void add(const P_<double(P_<const Arg_1>, P_<const Arg_2>)> f)
{
add_wrapped<Arg_1, Arg_2>(f);
}
Dispatcher()
{
add(sum_ss);
add(sum_si);
add(sum_is);
add(sum_ii);
}
};
const Dispatcher sum;
void run()
{
const Base& s = String();
const Base& i = Integer();
const Base& b = Base();
sum(&s, &s);
sum(&s, &i);
sum(&i, &s);
sum(&i, &i);
// Bang
sum(&b, &b);
}
} // namespace app
int main()
{
using namespace std;
try
{
app::run();
return EXIT_SUCCESS;
}
catch (const exception& x)
{
cerr << "!oops - " << x.what() << endl;
}
return EXIT_FAILURE;
}

It can be changed a little bit more to be more familiar, for example I don't see the point of

template<class Type> using P_ = Type*;

In fact, it can be eliminated in favor of the classic Type* instead of P_<Type>.

Anyway, it's a matter of taste, I suppose

Notes on C++14 and C++17

In order to make it possible to compile it with a C++14 compliant compiler, I didn't do very much.

Apparently, in C++17, and not before this version of the standard, they thought it would be nice to have a list after using, rather than forcing the programmer to write a list of using.

So

using cppx::P_;
using cppx::hopefully;
using cppx::fail;
// ...

can be written as

using cppx::P_,
      cppx::hopefully,
      cppx::fail;

Also, in C++17 a is_polymorphic_v was added, which is simply this:

template<class T>
constexpr bool is_polymorphic_v = is_polymorphic<T>::value;

I am always amazed by the holes in this incredible, huge, language. I've always seen it as insufficient, confusing and ugly to be programmed in. Not ideal for OO paradigm. Almost unusable, unless you stick to a subset to use it like a sort of enhanced C with patches of object orientation on it.

C++11 made it better — I believe auto, decltype and lambdas were key additions. It was a hope; but when I find changes like those I've shown above, I start to wonder what went wrong in the standardization process. Because there must be a story, a good one, for those things — the more they look minor, the more the story must be good.

No comments:

Post a Comment