chaining callables in C++

353 Views Asked by At

I come from the python world where I could define a chain of operations and call them in a for loop:

class AddOne:
    def __call__(self, x, **common_kwargs):
        return x+1
class Stringify:
    def __call__(self, x, **common_kwargs):
        return str(x)
class WrapNicely:
    def __call__(self, s, **common_kwargs):
        return "result="+s
data = 42
for operation in [AddOne(), Stringify(), WrapNicely()]:
    data = operation(data)
output = data

(Note: the goal is to have complex operations. Ideally, common kwargs could be given)

What would be the equivalent in C++ if the return type can be different after each call?

I'm not sure I could find anything close but I may have search with wrong keywords…

3

There are 3 best solutions below

2
On BEST ANSWER

C++ is statically typed, so options here are limited:

  • Create a chain of functions that can be determined at compile time.
  • Create functions with parameter and return type being the same
  • Return a type that could "store multiple alternative types" such as std::variant

For the first alternative you could create a class template that executes functions via recursive calls, but it's a bit more complex than your python code:

template<class...Fs>
class Functions
{
    std::tuple<Fs...> m_functions;

    template<size_t index, class Arg>
    decltype(auto) CallHelper(Arg&& arg)
    {
        if constexpr (index == 0)
        {
            return std::forward<Arg>(arg);
        }
        else
        {
            return std::get<index - 1>(m_functions)(CallHelper<index - 1>(std::forward<Arg>(arg)));
        }
    }

public:
    Functions(Fs...functions)
        : m_functions(functions...)
    {
    }

    template<class Arg>
    decltype(auto) operator()(Arg&& arg)
    {
        return CallHelper<sizeof...(Fs)>(std::forward<Arg>(arg));
    }
};

int main() {
    Functions f{
        [](int x) { return x + 1; },
        [](int x) { return std::to_string(x); },
        [](std::string const& s) { return "result=" + s; }
    };

    std::cout << f(42) << '\n';
}

Note: This requires the use of a C++ standard of at least C++17.

2
On

TL;DR

Use composition from ranges:

using std::views::transform;

auto fgh = transform(h) | transform(g) | transform(f);
auto fgh_x = std::array{42} | fgh; // Calculate f(g(h(x)))
// single element range ^^
//                      ^^ ranges::single_view{42} is an alternative

std::cout << fgh_x[0]; // Result is the only element in the array.

Demo


DIY

I've written a series of articles on C++ functional programming years ago, for some thoughts on composition you can start from this one.

That said, you can also avoid the "functional nuances" and start from scratch. Here is a generic composer of callables:

template <class F, class... Fs>
auto composer(F&& arg, Fs&&... args)
{
    return [fun = std::forward<F>(arg), 
            ...functions = std::forward<Fs>(args)]<class X>(X&& x) mutable {
        if constexpr (sizeof...(Fs))
        {
            return composer(std::forward<Fs>(functions)...)(
                std::invoke(std::forward<F>(fun), std::forward<X>(x)));
        }
        else
        {
            return std::invoke(std::forward<F>(fun), std::forward<X>(x));
        }
    };
}

which you'd use as:

// Store the composed function or call it right away.
composer(lambda1, lambda2, lambda3)(42); 

Demo

4
On

When teaching C++ to python developers, you've got to be careful in order to overcome the "C++ is so complicated" prejudice.

In this regard, you have two options:

  • If you want to chain operations, you can directly nest lambdas just as in python. It's only a different syntax, see my anser below.

  • However, if you use the chaining more often and want to apply the linear compose(f,g,h) syntax (which save you from typing a few char's), you should generate a composer yourself. The other answers follow this path, and for brevity I'd suggest the answer of @NikosAthanasiou.

So, here is the short version: Given some variable x and assuming it is a number (as you apply +1), you can directly chain the lambdas:

auto operation = [](auto x) { return [](auto y) { return "result="+std::to_string(y); }(x+1); };

ans use it as

std::vector<int> v;  // -> fill the vector v 
std::vector<std::string> w;
for(auto& x : v)
{
    w.push_back(operation(x));
}

Only thing which you miss is the in-place mutation from int to string. For this, see the other answers using a std::variant, but why you should? ... use it only when you really need it.