【问题标题】:C++20 functional-style function callsC++20 函数式函数调用
【发布时间】:2021-03-22 06:52:21
【问题描述】:

我最近一直在努力实现以函数式(管道)样式调用函数的方法,例如给定函数 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++ 抱怨(重新)定义完全相同的模板

有没有办法实现如上所述的函数流水线?

【问题讨论】:

  • 不清楚你想要什么,args是什么。
  • 你不能使用函子(有重载operator())而不是函数。
  • “无法传递函数名”,通过名称传递重载集确实是不可能的,但是lambda可以用于。
  • 在全局范围内_add 是一个保留符号。可能是导致某些问题的原因。
  • 我已将此标记为需要更多详细信息。它需要一个可重现的例子。至少。

标签: c++ functional-programming template-specialization c++20


【解决方案1】:

只需将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|

【讨论】:

  • 我会避免泛型函数,这可能会冲突/禁止其他自定义 operator|,最好创建自定义包装器类型 IMO。
  • @Jarod42 我也愿意,但这会让事情变得更加复杂。无论如何都完成了。
【解决方案2】:

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.

Example.

【讨论】:

    【解决方案3】:

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

    对于内置和非依赖类型,是的。 对于依赖类型,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>;
    

    Demo.

    【讨论】:

    • 稍微扩展了 args 以匹配多个参数,它就像一个魅力。我不知道std::forward 可以这样使用。非常感谢,很高兴学到新东西:3
    • 虽然它仍然需要在别名之前定义函数,否则它们的使用会导致“未声明”错误,例如error: ‘_add’ was not declared in this scope。目前正在尝试以我的方式提供正确的包含顺序,尽管我认为这很成问题。
    • 同时重新声明别名会引发“冲突声明”错误,因此我无法在每次声明重载时重新定义它,但不确定这是否是正确的寻址方式这一点
    • 正如我所说,对于内置/非依赖类型,您需要尊重顺序。有adl_tag这样的技巧来启用ADL。
    猜你喜欢
    • 2021-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-07
    • 1970-01-01
    • 2016-11-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多