С++可变参数模板:实现可变参数仿函数

问题描述 投票:2回答:2

我的同事给了我一个“小小测验”,他让他的学生解决了一次。似乎我虚弱的头脑无法理解现代C ++功能的所有美感。

主旨:

实现join函数,接受任意仿函数并返回另一个函子,它们的行为与其中任何一个一样。例:

{
    auto result = std::visit(custom::join(
        [](std::string const& s) { return "it's a string"; },
        [](std::pair<int, int> const& p) { return "it's a pair"; }
    ), var);

    assert(result == "it's a string");

    var = std::make_pair(10, 20);

    auto lvalue_lambda = [](std::string const& s) { return "it's a string"; };
    result = std::visit(custom::join(
        lvalue_lambda,
        [](std::pair<int, int> const& p) { return "it's a pair"; }
    ), var);

    assert(result == "it's a pair");
}

好吧,经过一番思考后我得到了这个想法,std::variant的意思是“列出的一个”,只要它是一个“类型安全联盟”,所以我需要一个元组。试过这样的事情:

namespace custom
{
    template<typename ...Functors>
    class ResultFunctor
    {
    public:
        ResultFunctor(Functors&&... funcs)
            : m_funcs(std::make_tuple(std::move(funcs)...))
        {}

        template<typename ...Params>
        auto operator()(Params... params) // that's where I got stuck
        {
//            return std::get<void(Params...)>(m_funcs)(params...); // No, the return type spoils this idea
            return std::get<0>(m_funcs)(params...);  // Now I need to choose the correct functor
        }

    private:
        std::tuple<Functors...> m_funcs;
    };

    template<typename ...Functors>
    ResultFunctor<Functors...> join(Functors&&... funcs)
    {
        return ResultFunctor(std::move(funcs)...);
    }
}

如果它只适用于具有void返回类型的算符,我很容易得到元组所需的元素。但似乎没有办法确定它,返回类型不能从给定的参数中推断出来(显然)。

另一个想法是使用一些SFINAE技巧来选择正确的operator()()版本,但这种方式或其他我必须“贯穿”所有元组项目(这是令人讨厌的,但仍然可以谷歌搜索),然后检查是否该项目基于给定的参数包是合适的。

嗯,这就是我停下来彻底思考它的地方。如果任何人(谁能更好地处理所有可变的东西)会有任何想法,我会非常感激。

c++ c++17 variadic-templates sfinae
2个回答
4
投票
namespace custom {
  template<class...Fs>
  struct overloaded : Fs... {
      using Fs::operator()...;
  };
  template<class...Fs>
  overloaded(Fs...)->overloaded<Fs...>;

  template<class F>
  F&& as_obj( F&& f ){ return std::forward<F>(f); }
  template<class R, class...Args>
  auto as_obj( R(*f)(Args...) {
    struct helper {
      R(*f)(Args...);
      R operator()(Args...args) const { return f(std::forward<Args>(args)...); }
    };
    return helper{f};
  }

  template<class...Fs>
  auto join( Fs&&...fs ){
    return overloaded{as_obj(std::forward<Fs>(fs))...};
  }
}

我添加了作为非重载函数指针的奖励支持。


6
投票

这有一个非常简单的解决方案,不涉及SFINAE或模板元编程(只是常规模板)。

第一步是编写一个代表联合过载集的仿函数。这很容易通过继承实现,并且因为用作输入的所有函子必须具有不同的类型,所以我们不必做任何花哨的事情。

// This represents overload set
template<class F1, class F2>
struct Joint : public F1, public F2 {
    using F1::operator(); 
    using F2::operator(); 
}; 

为方便用户,我们可以添加演绎指南:

template<class F1, class F2>
Joint(F1, F2) -> Joint<F1, F2>; 

因为Joint是C ++ 17及更高版本中的聚合类型,所以我们不必提供构造函数,因为我们可以使用聚合初始化:

// This code magically works
auto result = std::visit(Joint{
    [](std::string const& s) { return "it's a string"; },
    [](std::pair<int, int> const& p) { return "it's a pair"; }
}, var);

编写custom::join函数同样简单:

template<class F1, class F2>
auto join(F1&& f1, F2&& f2) {
    return Joint { std::forward<F1>(f1), std::forward<F2>(f2) }; 
}

现在我们已经有了基本情况,我们可以很容易地概括它:

template<class F, class F2, class... Fs>
auto join(F&& f, F2&& f2, Fs&&... fs) {
    return Joint{
        std::forward<F>(f),
        join(std::forward<F2>(f2), std::forward<Fs>(fs)...)
    };
}

Addressing potential criticisms

  • 为什么不为Joint定义构造函数?聚合初始化是最有效的初始化形式,因为当您没有定义构造函数时,编译器能够就地分配值而无需复制或移动它们。
  • 为什么要使用多重继承?如果我们依赖SFINAE,那会增加编译时间,增加代码复杂性,并且在某些情况下它无法正常工作。使用SFINAE,您必须检查过载集的每个成员以查看它是否合适。在某些情况下,由于隐式转换,选择更严重的过载,因为它是匹配的。通过使用继承,我们可以使用语言的内置模式匹配进行函数调用。
  • 为什么要添加演绎指南?它们使代码更清晰,在这种情况下它们完全按预期工作:参数按值存储
© www.soinside.com 2019 - 2024. All rights reserved.