【问题标题】:How do I write a function that concatenates two functions?如何编写连接两个函数的函数?
【发布时间】:2023-03-07 14:19:01
【问题描述】:

我正在尝试编写一个通用函数,它连接两个可以使用同一组参数调用的函数,但我遇到了一些麻烦。这是我到目前为止所拥有的(它无法编译)

//A functor to store the input functions and call them
template <typename LEFT, typename RIGHT>
struct combine_functions {
  combine_functions(const LEFT &left, const RIGHT &right)
   : left(left), right(right) {}

  template <typename ...ARGS>
  std::enable_if_t<
    //My compiler doesn't have support for C++17 std library so I 
    //found an implementation of callable on SO
    is_callable_v<LEFT, std::decay_t<ARGS>...> &&
    is_callable_v<RIGHT, std::decay_t<ARGS>...>
  > operator()(ARGS... args) const {
    //the return value doesn't matter in my situation and can be 
    //completely discarded
    left(std::forward<ARGS>(args)...);
    right(std::forward<ARGS>(args)...);
  }

private:
  mutable LEFT left;
  mutable RIGHT right;
};

//I should probably have an enable if that checks the arguments 
//are function pointers or functors
template <typename LEFT, typename RIGHT>
combine_functions<
  std::decay_t<LEFT>,
  std::decay_t<RIGHT>
>
operator+(
  const LEFT &left,
  const RIGHT &right
) {
  return {left, right};
}

如果不清楚我要达到的目标,那么这里是一个测试。

#include <iostream>
#include "combine functions.hpp"    

struct A {
  void operator()(float &f, int i) {
    std::cout << "running A with float " << f << " and int " << i << '\n';
    f++;
  }
};

struct B {
  void operator()(float &f, int i) {
    std::cout << "running B with float " << f << " and int " << i << '\n';
    f++;
  }
};

struct C {
  void operator()(float &f, int i) {
    std::cout << "running C with float " << f << " and int " << i << '\n';
    f++;
  }
};

int main(int, const char**) {
  A a;
  B b;
  C c;
  auto abc = concat(concat(a, b), c);
  //or
  //auto abc = a + b + c;
  std::function<void(float &, int)> abcFunc = abc;
  float f = 5.0f;
  int i = 9;
  abcFunc(f, i);

  return EXIT_SUCCESS;
}

这是预期的输出

running A with float 5 and int 9
running B with float 6 and int 9
running C with float 7 and int 9    
  • 如何在 C++ 中实现这一点?
  • 在这种情况下使用重载运算符是否不明智?
  • “连接”是此操作的最佳术语吗?

【问题讨论】:

  • enable_if_t 基本上是没必要的。发布无法编译的内容,出现错误
  • 您使用的是什么编译器,遇到了什么错误?一个可疑的部分(在编译时不会影响任何东西)是在同一个参数上使用 std::forward 两次 - 如果第一个函数消耗一个右值,那么第二个函数就没有任何东西了。
  • 请提供minimal reproducible example,包括您使用的任何第三方代码。不完整的片段是没有意义的。错误可能在任何地方。
  • @n.m.通常是的。我认为这是可以接受的,因为 OP 不知道 如何 处理此代码。就期望的结果而言,这是最少且有据可查的,并且他们在尝试解决问题时表现出了尽职调查。
  • 您的 operator+ 将适用于所有类型的对象,甚至是不可调用的对象。这可能是您想要 enable_if 的地方。但是为您没有创建的任何类创建重载运算符通常是不好的做法,并且您没有编写 std::function 类或函数指针“类”,所以我会完全放弃它。此外,我希望(f+g)()f()+g() 相同;见function spaces on Wikipedia

标签: c++ templates lambda variadic-templates


【解决方案1】:

我认为这是一个合理的起点。通过完美转发支持任意数量的连接和任意数量的参数:

#include <tuple>
#include <utility>
#include <iostream>

namespace detail 
{
    template<class Tuple, std::size_t...Is, class...Args>
    void exec(Tuple&& tuple, std::index_sequence<Is...>, Args&&...args)
    {
        using expand = int[];
        void(expand{
            0,
            (std::get<Is>(tuple)(std::forward<Args>(args)...),0)...
        });

    }
}

template<class...Funcs>
auto concat(Funcs&&...funcs)
{
    constexpr auto nof_funcs = sizeof...(funcs);
    return [funcs = std::make_tuple(std::forward<Funcs>(funcs)...)](auto&&...args) mutable
    {
        detail::exec(funcs, 
                     std::make_index_sequence<nof_funcs>(), 
                     std::forward<decltype(args)>(args)...);
    };
};

int main()
{
    auto f1 = [](auto&& arg) { std::cout << arg << std::endl; };
    auto f2 = [](auto&& arg) { std::cerr << arg << std::endl; };

    concat(f1, f2)("Hello, World");
}

【讨论】:

  • 我将它插入到我的代码库中,它运行良好,但我并不真正理解代码。如果不真正理解它,我会使用别人的代码感到内疚。你能详细描述一下它的作用以及它是如何做到的吗?另外,在我认为您调用函数的地方,您使用的是forward。但 Jarod42 表示这样做会多次移动对象。
  • @Kerndog73 要了解它的作用,请查看std::apply 的文档。至于转发,这里除了expand外,没有包被转发两次。 args 不应该在那里转发
  • @Rerito 我可以想到转发参数正确的情况,以及不正确的情况。也许更好的解决方案是编写一些代码来检测引用,然后适当地传递 std::ref/cref。因此是“起点”。完全通用的解决方案将是不平凡的。
【解决方案2】:

您可以使用以下内容:

template <typename LEFT, typename RIGHT>
struct combine_functions {
private:
  LEFT left;
  RIGHT right;
public:
  combine_functions(const LEFT& left, const RIGHT& right)
   : left(left), right(right) {}

  template <typename ...ARGS>
  auto operator()(ARGS&... args) const
  -> decltype(left(args...), static_cast<void>(right(args...)))
  {
    left(args...);
    right(args...);
  }

};

template <typename LEFT, typename RIGHT>
combine_functions<std::decay_t<LEFT>, std::decay_t<RIGHT>>
concat(const LEFT& left, const RIGHT& right)
{
  return {left, right};
}

Demo

我不使用operator +,它太通用并且匹配太多类型。 我不使用std::forward,因为您不想使用moveright 会调用移动对象...)

【讨论】:

  • 其实只能在第二次通话时转接!
  • @Rerito:我认为您对错误答案的评论 ;-) ...请参阅 (at)PasserBy 的答案。
  • 您的返回类型operator() 是否意味着(表达)SFINAE'ing?还是为了什么?
  • @davidhigh 不,但你是对的,forward 上的另一个答案是错误的。我的意思是他仍然可以使用std::forward,但只能在第二次通话时使用
  • @davidhigh:是的,它适用于 SFINAE
【解决方案3】:

如何在 C++ 中实现这一点?

我通常不只是为某人编写代码,但这很简单。

#include <iostream>
#include <functional>
#include <string>

using namespace std;

template <typename Left, typename Right>
class ConcatFn {
public:
    ConcatFn(Left left, Right right)
            : left(left)
            , right(right) {
    }

    template <typename... Args>
    void operator()(Args... args) {
        this->left(args...);
        this->right(args...);
    }

private:
    function<Left> left;
    function<Right> right;
};

void A(const char *foo) {
    cout << "A: " << foo << endl;
}

void B(string bar) {
    cout << "B: " << bar << endl;
}

int main() {
    ConcatFn<void(const char *), void(string)> fn(A, B);

    fn("hello!");

    return 0;
}

输出:

$ ./concat
A: hello!
B: hello!

我认为您不会摆脱上面fn 声明中的模板参数。

另外,如果您想保证函数具有准确的签名,只需删除第二个模板参数 (Right) 并在使用 Right 的任何地方使用 Left(最好重命名)。

在这种情况下使用重载运算符是否不明智?

一点也不。

“连接”是此操作的最佳术语吗?

可能不会,但鉴于此用例很少见,我不知道有任何“标准”术语。可能是函数链接或分组?

【讨论】:

    【解决方案4】:

    看起来不错,只是这里和那里有一些怪癖

    #include<type_traits>
    #include<utility>
    
    template<typename L, typename R>
    struct combined
    {
        typename std::decay<L>::type l;
        typename std::decay<R>::type r;
        combined(L&& l, R&& r)
           : l(std::forward<L>(l)), r(std::forward<R>(r)) {}
    
        template<typename... Args>
        void operator()(Args&&... args)
        {
            l(args...);
            r(std::forward<Args>(args)...);
        }
    };
    
    template<typename L, typename R>
    combined<L, R> combine(L&& l, R&& r)
    {
        return {std::forward<L>(l), std::forward<R>(r)};
    }
    

    首先,最有可能必须存储可调用对象,因此std::remove_reference

    std::enable_if ai 是不必要的,因为当对象调用不正确时编译器会发出错误。

    std::forward 被称为perfect forwarding,很重要。

    EDIT3经过辩论,第一个 forward 被删除以防止发生意外动作,这要感谢 Rerito、R. Martinho Fernandes 和 Jarod42

    operator+感觉很奇怪,因为你没有添加返回值,这与函数添加的定义冲突。

    【讨论】:

    • 一旦你std::forward-ed 他们就不能使用args...,因为包携带的一些参数可能已经被移动了......删除对std::forward的第一个调用:@ 987654331@。如果你想在其参数上使用 forward ,你的构造函数应该被模板化。
    • 一开始没想过搬家会出问题,只是觉得应该完美转发。经过一番思考,我决定我的意思是,这应该只是围绕调用两个函数的一些语法糖,如果用户不想移动,就不要给一个移动的函数。
    • @Rerito 为什么我需要对构造函数进行模板化?
    • 因为forward 应该与模板参数推导一起使用。否则没有意义。假设Lstd::function&lt;void(int)&gt;,那么你的构造函数只接受std::function&lt;void(int)&gt;&amp;&amp;。这只能绑定右值...不是完美转发的目标吗?
    • 我的错,我没有注意到助手。但是,我仍然信守诺言,双重forward 可能有害。 forwarding 与模板参数推导一起使用(因此全部取决于在每个调用站点传递的参数的值类别)。如果用户使用纯右值调用,那么他可能会在该实现中遇到麻烦
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-02
    • 2015-04-06
    • 1970-01-01
    相关资源
    最近更新 更多