Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

chaining callables in C++

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…

like image 268
Jav Avatar asked Mar 13 '26 09:03

Jav


2 Answers

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.

like image 198
fabian Avatar answered Mar 14 '26 23:03

fabian


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

like image 24
Nikos Athanasiou Avatar answered Mar 14 '26 23:03

Nikos Athanasiou



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!