C++20 functional-style function calls

Viewed 185

I have been recently struggling to achieve a way of calling functions in a functional (piped) style such as given a function foo(int, float) can be called in such way 10 | foo(10.f) .

(Assuming the pipe operator here, which can actually be random)

This is as far as I've gotten

string _add(int&& a1, int&& a2)
{
    return "ADD i:" + to_string(a1) + " i:" + to_string(a2);
}

string _add(float&& a1, int&& a2)
{
    return "ADD f:" + to_string(a1) + " i:" + to_string(a2);
}

string _add(int&& a1, float&& a2)
{
    return "ADD i:" + to_string(a1) + " f:" + to_string(a2);
}

struct add_name;
template <typename SUBJ, typename A1>
auto operator|(SUBJ subj, args<add_name, A1, monostate> args)
{
    return _add(move(subj), move(args.a1));
}
template <typename A1> using add = args<add_name, A1, monostate>;

where args is a template structure to hold arguments. This allows me to invoke the functions like this

string s1 = 10.f | add(15);
string s2 = 10 | add(15);
string s3 = 10 | add(10.f);

Eventually there is a macro that generates the templates for each function name and I am able to use this quite successfully. Also, this works very well for overloads and function templates.

There is, however a huge drawback to this, i.e all function specializations need to be known prior the definition of the operator template.

It makes it more and more challenging to use, while the codebase grows.

I've been back and forth with different ideas, but there are C++ limitations that does not allow me to implement this the way it would be much more useful, including

  1. no way of passing a function name down the template specialization chain
  2. C++ complaining about (re)defining the exact same template

Is there a way to achieve function pipelining as described above ?

3 Answers

all function specializations need to be known prior the definition of the operator template.

For builtin and non-dependant type, yes. For dependent type, ADL might help.

no way of passing a function name down the template specialization chain

Passing overloads set cannot be passed by name, but can be by lambda.

C++ complaining about (re)defining the exact same template

You have tag, to allow your different overload, so it shouldn't be problematic.

But, you might write only one operator| if you provide Functor as template parameter, something like:

template <auto, typename T>
struct Args
{
    Args(T data) : data(std::move(data)) {}
    
    T data;  
};

template <typename LHS, auto F, typename T>
auto operator|(LHS&& lhs, Args<F, T> rhs)
{
    return F(std::forward<LHS>(lhs), std::move(rhs.data));
}

then

// Your overloads
std::string _add(int&& a1, int&& a2)
{
    return "ADD i:" + std::to_string(a1) + " i:" + std::to_string(a2);
}

std::string _add(float&& a1, int&& a2)
{
    return "ADD f:" + std::to_string(a1) + " i:" + std::to_string(a2);
}

std::string _add(int&& a1, float&& a2)
{
    return "ADD i:" + std::to_string(a1) + " f:" + std::to_string(a2);
}

// The alias
template <typename T>
using add = Args<[](auto&& lhs, auto&& rhs){ return _add((decltype(lhs))lhs, (decltype(rhs))rhs); }, T>;

Demo.

Just make operator| generic:

template<typename LeftT, typename RightT>
auto operator|(LeftT&& left, RightT&& right)
-> decltype(std::forward<RightT>(right)(std::forward<LeftT>(left)))
{return std::forward<RightT>(right)(std::forward<LeftT>(left));}

Now This allows you to value | single_arg_method for any value and unary functionoid in the entire codebase. (Though this will complicate your compiler errors any time you have a typo near a an innocent |)

Then the only trick is to make add(15) generate the desired unary functionoid. I was unable to think of a way to do this without macros to give this functionoid the same name as the method it called.

#define define_bifunction_to_monofunction(name) \
template<class ArgT> \
auto name(ArgT&& right) { \
    return [&right](auto&& left){ \
        return _ ## name(std::forward<decltype(left)>(left), std::forward<ArgT>(right)); \
    }; \
}

(Someone with more knowledge than I of macros might now how to use ## to get it to call _add instead of add)

Now the usage ends up simple:

string _add(int&& a1, float&& a2)
{
    return "ADD i:" + to_string(a1) + " f:" + to_string(a2);
}
define_bifunction_to_monofunction(add)

int main() {
    string s1 = 10.f | add(15);
    string s2 = 10 | add(15);
    string s3 = 10 | add(10.f);
}

http://coliru.stacked-crooked.com/a/30697aebc0d594ac

However, this does make the operator| overly aggressive, which can lead to strange compiler errors, so it's usually recommended to make some sort of type flag, which complicates the implementation slightly:

template<typename FunctionT>
struct enabled_pipe_chaining : std::false_type {};

template<typename LeftT, typename RightT, typename enabled=std::enable_if_t<enabled_pipe_chaining<RightT>::value,void> >
auto operator|(LeftT&& left, RightT&& right)
{return std::forward<RightT>(right)(std::forward<LeftT>(left));}

#define enable_mono_pipe_chain(name) \
template<> struct enabled_pipe_chaining<decltype(name)> : std::true_type {};

#define define_bifunction_pipe_chain(name) \
template<typename RightT> \
struct name ## _pipe_chainable { \
    RightT&& right; \
    template<typename LeftT> \
    auto operator()(LeftT&& left) { \
        return _ ## name(std::forward<decltype(left)>(left), std::forward<RightT>(right)); \
    } \
}; \
template<typename RightT> struct enabled_pipe_chaining<name ## _pipe_chainable<RightT>> : std::true_type {}; \
template<typename RightT> \
name ## _pipe_chainable<RightT> name(RightT&& right) { return {std::forward<RightT>(right)};}

http://coliru.stacked-crooked.com/a/41ce66c1a1444495

But then the operator| will only be available for pipe_chainable methods, rather than any operator| in the code.

Clang will tell you what the problem is with declaring the overloads after the template:

error: call to function '_add' that is neither visible in the template definition nor found by argument-dependent lookup
    return _add(move(subj), move(args.a1));
           ^

The simplest solution is to enable argument-dependent lookup by passing a tag type:

struct adl_tag {};
template <typename SUBJ, typename A1>
auto operator|(SUBJ subj, args<add_name, A1, monostate> args)
{
    return _add(adl_tag{}, move(subj), move(args.a1));
}
string _add(adl_tag, int&& a1, int&& a2)
// etc.

Example.

Related