Polymorphic (Generic) Functions as Arguments in C++

497 views Asked by At

I am developing a relatively simple program (a calculator actually). However, I have decided to make all components of my program as generic as possible because:

  1. It's good practice.
  2. It keeps things interesting.

As part of this program I am using a Tuple class that I am writing. I know that a class already exists, but I like having complete control over my code and this is only an exercise.

One thing I need to do is transform a tuple of expressions (where expressions themselves are generic) into a tuple containing the result of the expressions' evaluations. In short, I have (with trivial parts left out):

template <class T>
class Expression {

    public:
        virtual T Eval() = 0;

    // ...
};

template <class First, class ... Rest>
class Tuple {

    // ...

    private:
        First first;
        Tuple<Rest ...> rest;
};

And I would like to specialize over a tuple of a generic type like this:

template <template <class> class R, class First, class ... Rest>
class Tuple<R<First>, R<Rest> ...> {

    // and here is the problem:
    Tuple<First, Rest ...> Transform(function<template<class T> T(R<T>)>);
};

After which I could do this:

template <class T> // There has to be a better way to do this
T Eval(Expression<T>& expr){
    return expr.Eval();
}

// ...
Tuple<First, Rest ...> tuple = exprs.Transform(Eval);

There are a few places here where I am not sure how to go about things and a real expert who could help me out here would be appreciated. I expect this code will not compile because of minor flaws but that isn't the point - My primary worry is the line I marked. If I recall correctly from the brief period I learned Haskell this function should be of Rank-2 (If not please comment and I will remove the tag). It just doesn't look right. Is there any way to do this?

Update:

I was advised to try pass a functor with a generic operator () as a template argument but that didn't work either.

2

There are 2 answers

0
Barry On BEST ANSWER

I think you can do this pretty simply without C++14 at all. I'm going to assume a few things about how your Tuple is constructed, namely that these two ctors exist:

Tuple(First, Rest... );                // (1)
Tuple(First, const Tuple<Rest...>& );  // (2)

We need one type trait: given a function that we're transforming with, we need to know what types it produces:

template <typename T, typename F>
using apply_t = decltype(std::declval<F>()(std::declval<T>()));

(God I love C++11)

With that, we can determine the return type easily, and it's just a matter of calling the function recursively:

template <typename First, typename... Rest>
struct Tuple
{
    template <typename F>
    Tuple<apply_t<First, F>, apply_t<Rest, F>...>
    Transform(F func)
    {
        return {func(first), rest.Transform(func)}; // hence the need
                                                    // for ctor (2)
    };
};

(Depending on how you wrote your Tuple you may or may not need a base case for a trivial transform that just returns a Tuple<>, or a base case that just returns a Tuple<apply_t<First, F>>. Either way, not a big deal).

And you don't even need to specialize Tuple at all. You just need to pass in the right functors. For instance:

struct Zero
{
    template <typename T>
    int operator()(T ) { return 0; }
};

struct Incr
{
    template <typename T>
    T operator()(T x) { return x + 1; }
};

Tuple<int, double, char> tup(1, 2.0, 'c');
auto z = tup.Transform(Zero{}); // z is Tuple<int, int, int>{0, 0, 0}
auto i = tup.Transform(Incr{}); // i is Tuple<int, double, char>{2, 3.0, 'd'}

Here's a full code example, logging all the types too. Of course, with C++14, we can do those inline:

auto i2 = tup.Transfom([](auto x) -> decltype(x) {return x+1; });
// i2 is a Tuple<int, double, char>{2, 3.0, 'd'};
// without the trailing decltype, it gets deduced as Tuple<int, double, int>.
8
davidhigh On

The usual trick in C++14 is to use some index_sequence (see here) and then something like:

template<typename ... Args, size_t ... I>
auto evaluate(Tuple<Args ...> const& t, index_sequence<I...>)
{
    return make_tuple(evaluate(get<I>(t))...);
}

See, e.g., this answer for an example of this approach (the only difference is that here additionally a function call is invoked).

Thus, what you need here in your Tuple class for this is:

  • An implementation of a custom get function which behaves similarly to std::get, i.e. accepts variadic index arguments.
  • An implementation of a custom make_tuple function which behaves similarly to std::make_tuple and constructs a tuple from a comma-separated list.

Further, you require a function template evaluate which is able to evaluate a single expression, but I guess you have this already.


EDIT: I just realized that the above might not be very helpful for you. Rather it should be noted that you can do that also recursively:

template<typename ... Args>
auto evaluate(Tuple<Args ...> const& t)
{
    return tuple_cat(make_tuple(evaluate(t.first)), evaluate(t.rest));
}

template<typename T> auto evaluate(Tuple<T> const& t) { return evaluate(t.first); }

Again, you require a make_tuple function, a tuple concatenator tuple_cat and a single-expression evaluator evaluate.