C++20 函数式编程风格的函数调用

3

最近我一直在努力寻找一种函数式(管道)风格的调用函数的方法,比如一个函数foo(int, float)可以这样调用:10 | foo(10.f)

(假设这里使用了管道操作符,实际上可以是任意的)

这是目前我所达到的程度。

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>;

args 是一个模板结构,用于保存参数。这样我就可以像这样调用函数:

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

最终,有一个宏可以为每个函数名生成模板,我能够很成功地使用它。

对于重载和函数模板,这也非常有效。

然而,这里有一个巨大的缺点,也就是所有函数特化都需要在运算符模板定义之前知道。

随着代码库的增长,这使得使用越来越具有挑战性。

我一直在琢磨不同的想法,但有一些C++的限制阻止了我以更加有用的方式来实现它,包括:

  1. 没有办法将函数名称沿着模板专业化链传递
  2. C++抱怨(重新)定义相同的模板

是否有一种方法可以实现如上所述的函数流水线处理?


2
不清楚你想要什么,args是什么。 - Jarod42
你不能使用函数对象(带有重载的 operator())代替函数吗? - Jarod42
1
“没有传递函数名称的方法”,通过名称传递重载集是不可能的,但可以使用lambda表达式来实现。 - Jarod42
在全局范围内,_add 是一个保留符号。可能会导致一些问题。 - Eljay
我已将此标记为“需要更多细节”。它需要一个可重现的示例。至少一个。 - Enlico
3个回答

1

所有函数特化都需要在运算符模板定义之前知道。

对于内置和非依赖类型,是的。 对于依赖类型,ADL可能会有帮助。

没有方法可以将函数名称传递到模板特化链中

重载集合不能通过名称传递,但可以通过lambda表达式传递。

C++抱怨(重新)定义完全相同的模板

您有标记,以允许不同的重载,因此不应该有问题。

但是,如果您提供Functor作为模板参数,则可以仅编写一个operator|,例如:

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));
}

那么

// 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>;

演示.


将args扩展一下以匹配多个参数,它就能完美运行了。我不知道std::forward可以这样使用。非常感谢,很高兴学到了新东西 :3 - foo
虽然仍需要在别名之前定义函数,否则使用将导致“未声明”的错误,例如 error: ‘_add’ was not declared in this scope。目前正在尝试使我提供正确的包含顺序,尽管我认为这相当棘手。 - foo
同时重新声明别名会引发“冲突声明”错误,因此我无法在每次声明重载时重新定义它,尽管不确定这是否是解决此问题的正确方法。 - foo
1
就像我之前所说的,使用内置/非依赖类型时,你需要遵守顺序。有一些技巧,比如使用 adl_tag 来启用 ADL。 - Jarod42

0

operator|泛化:

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));}

现在,这允许您对整个代码库中的任何值和一元函数应用value | single_arg_method,但是如果您在无辜的|附近打字错误,则会使编译器错误变得更加复杂。

然后唯一的技巧就是使add(15)生成所需的一元函数。我无法想到没有使用宏将此函数重命名为调用它的方法名称的方法。

#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)); \
    }; \
}

对于宏的了解比我更多的人可能知道如何使用##来调用_add而不是add

现在,使用变得简单:

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

然而,这确实使得operator|过于激进,可能会导致奇怪的编译器错误,因此通常建议制定某种类型标志,这会稍微复杂化实现:

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

但是operator|仅适用于pipe_chainable方法,而不是代码中的任何operator|


_ ## 名称? - Jarod42
我会避免使用通用函数,因为它可能会与其他自定义 operator| 冲突/禁止,最好在我的意见中创建自定义包装器类型。 - Jarod42
@Jarod42 我也想这样做,但是它会使事情变得非常复杂。不过还是完成了。 - Mooing Duck

-1

Clang会告诉你,在模板后声明重载函数的问题所在:

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));
           ^

最简单的解决方案是通过传递标签类型来启用参数相关查找:

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.

示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接