C++11中使用可变参数模板实现函数组合

5

我是一名数学家,长期以来习惯于进行“老式”C++编程。我感觉C++11提供的一些新的语法结构可以帮助我在我的专业项目中实现更好的代码。然而,由于我不是计算机专业人士,我必须承认我缺乏理解我自学过程中遇到一些例子所需的知识,尽管迄今为止我非常幸运/成功。

我的印象是可变参数模板可以用于实现类型安全的函数组合,就像这个问题中所示。我的关注点略微更为普遍,因为我希望用异构(但兼容)的参数/返回类型组合函数。我搜索了很多并找到另一个参考资料,但对我来说似乎是完全的“黑魔法”,;) 我不会假装我能够在我的环境中使用该代码,虽然我感觉我应该在那里找到我需要的东西。

我认为下面(最不完整)的代码相对自解释,说明我想要实现的内容。特别地,当试图组合不兼容的函数(这里是Arrow)时,我相信合适的实现将抛出编译时错误,并需要一段递归模板代码。

template <typename Source , typename Target> class Arrow
{
  Target eval (const Source &);
};

template <typename ...Arrows> class Compositor
{
  template <typename ...Arrows>
  Compositor (Arrows... arrows)
  {
     // do/call what needs be here
  };

  auto arrow(); // gives a function performing the functionnal composition of arrows

};

// define some classes A, B and C

int main(int argc, char **argv)
{
  Arrow < A , B >  arrow1;
  Arrow < B , C >  arrow2;

  Compositor< Arrow < A , B > , Arrow < B , C > > compositor(arrow1 , arrow2);

  Arrow < A , C >  expected_result = compositor.arrow();
}

理想情况下,我希望
    Compositor
可以直接继承
    Arrow < source_of_first_arrow , target_of_last_arrow>
并用对应的
    eval()
替换方法
    arrow()
但我觉得上面的代码更易于理解。
如果能提供现有(相对基础的)示例并指出我的搜索中可能遗漏的部分,将非常感激。谢谢!
2个回答

5

如果我理解正确,你不需要花哨的模板魔法来完成这个组合。这里是几乎自解释的代码:

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

// it is just an std::function taking A and returning B
template <typename A, typename B>
using Arrow = std::function<B(const A&)>;

// composition operator: just create a new composed arrow
template <typename A, typename B, typename C>
Arrow<A, C> operator*(const Arrow<A, B>& arr1, const Arrow<B, C>& arr2)
{
    // arr1 and arr2 are copied into the lambda, so we won't lose track of them
    return [arr1, arr2](const A& a) { return arr2(arr1(a)); };
}

int main()
{
    Arrow<int, float> plusHalf([](int i){return i + 0.5;});
    Arrow<float, std::string> toString([](float f){return std::to_string(f);});

    auto composed = plusHalf * toString; // composed is Arrow<int, std::string>
    std::cout << composed(6) << std::endl; // 6.5

    //auto badComposed = toString * plusHalf; // compile time error
}

我主要在这里使用了lambda函数。

使用单个函数调用而不是操作符链证明是一个更棘手的问题。这次你得到了一些模板:

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

// it is just an std::function taking A and returning B
template <typename A, typename B>
using Arrow = std::function<B(const A&)>;

// A helper struct as template function can't get partial specialization
template <typename... Funcs>
struct ComposerHelper;

// Base case: a single arrow
template <typename A, typename B>
struct ComposerHelper<Arrow<A, B>>
{
    static Arrow<A, B> compose(const Arrow<A, B>& arr)
    {
        return arr;
    }
};

// Tail case: more arrows
template <typename A, typename B, typename... Tail>
struct ComposerHelper<Arrow<A, B>, Tail...>
{
    // hard to know the exact return type here. Let the compiler figure out
    static auto compose(const Arrow<A, B>& arr, const Tail&... tail)
    -> decltype(arr * ComposerHelper<Tail...>::compose(tail...))
    {
        return arr * ComposerHelper<Tail...>::compose(tail...);
    }
};

// A simple function to call our helper struct
// again, hard to write the return type
template <typename... Funcs>
auto compose(const Funcs&... funcs)
-> decltype(ComposerHelper<Funcs...>::compose(funcs...))
{
    return ComposerHelper<Funcs...>::compose(funcs...);
}

using namespace std;

int main()
{
    Arrow<int, float> plusHalf([](int i){return i + 0.5;});
    Arrow<float, string> toString([](float f){return to_string(f);});
    Arrow<string, int> firstDigit([](const string& s){return s[0]-'0';});

    auto composed = compose(plusHalf, toString, firstDigit);
    // composed is Arrow<int, int>

    std::cout << composed(61) << std::endl; // "6"

    //auto badComposed = compose(toString, plusHalf); // compile time error
}

“我在这里主要使用了lambda函数”:是的,这正是让我感到害怕的地方;) 谢谢您(非常清晰)的建议,我会尝试将其适应于任意数量的函数组合的更复杂情况(因此使用可变参数模板标记)。鉴于您提供的基本构造块,这应该不太困难。 - user3144405
你可以使用这段代码组合超过两个函数。只需执行 auto f = f1 * f2 * f3 * f4; 即可。或者您想更像函数调用的方式吗?(auto f = compose(f1, f2, f3, f4);) - Guilherme Bernal
是的,我指的是后者。我想要的是可以写成compose(f1,...)的东西[但我似乎无法使内联代码字符样式起作用...?] - user3144405
是的,对我来说主要困难在于模板参数的递归,我想要弄清楚它。由于你们两个的解决方案性质不同,它们对我都很有启发性。感谢你的回答,我明天会尝试一下并保持联系。 - user3144405
我已经成功地将你的代码调整以适应我的需求。非常感谢你的帮助。 - user3144405
显示剩余2条评论

2

虽然输出结果完全相同,但我认为Arrow应该是自己的类,这样它的定义域和值域可以存储为类型。现在返回类型可以在不需要编译器推断的情况下得知。

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

// Revised as a class with types domain = A and range = B.  It still acts as a std::function<B(const A&)>, through its operator()(const A&) and its data member 'arrow'.
template <typename A, typename B>
class Arrow {
    const std::function<B(const A&)> arrow;
public:
    Arrow (const std::function<B(const A&)>& a) : arrow(a) {}
    B operator()(const A& a) const {return arrow(a);}
    using domain = A;
    using range = B;
};

// The overload * for the composition of two Arrows in Arrow's new form:
template <typename A, typename B, typename C>
Arrow<A,C> operator * (const Arrow<A,B>& arrow_ab, const Arrow<B,C>& arrow_bc) {
    return Arrow<A,C>([arrow_ab, arrow_bc](const A& a)->C {return arrow_bc(arrow_ab(a));});
}

template <typename First, typename... Rest> struct LastType : LastType<Rest...> {};
template <typename T> struct Identity { using type = T; };
template <typename Last> struct LastType<Last> : Identity<Last> {};

template <typename...> struct ComposeArrows;

template <typename First, typename... Rest>
struct ComposeArrows<First, Rest...> : ComposeArrows<Rest...> {
    using last_arrow = typename LastType<Rest...>::type;
    using composed_arrow = Arrow<typename First::domain, typename last_arrow::range>;
    composed_arrow operator()(const First& first, const Rest&... rest) {
        return first * ComposeArrows<Rest...>::operator()(rest...);
    }
};

template <typename Last>
struct ComposeArrows<Last> {
    Last operator()(const Last& arrow) {return arrow;}
};

template <typename... Arrows>
typename ComposeArrows<Arrows...>::composed_arrow composeArrows (const Arrows&... arrows) {
    return ComposeArrows<Arrows...>()(arrows...);
}

// ------------ Testing ------------
template <typename T>
std::string toString (const T& t) {
    std::ostringstream os;
    os << t;
    return os.str();
}

int main() {
    Arrow<int, double> plusHalf ([](int i){return i + 0.5;});
    Arrow<double, std::string> doubleToString ([](float f){return toString(f) + " is the result";});
    Arrow<std::string, char> lastCharacter ([](const std::string& str)->int {show(str)  return str.back();});

    const Arrow<int, char> composed = composeArrows (plusHalf, doubleToString, lastCharacter);
    std::cout << composed(2) << std::endl; // "t"
    std::cout << composeArrows (plusHalf, doubleToString, lastCharacter)(2) << std::endl; // "t"
}

以下是另一种语法略有不同的解决方案:

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

template <typename A, typename B>
struct Arrow {
    using domain = const A&;
    using range = B;
    using Function = std::function<range(domain)>;
    const Function& function;
    Arrow (const Function& f) : function(f) {}
    range operator()(domain x) const {return function(x);}
};

template <typename... Ts>
struct LastType {
    using Tuple = std::tuple<Ts...>;
    using type = typename std::tuple_element<std::tuple_size<Tuple>::value - 1, Tuple>::type;
};

template <typename Arrow1, typename Arrow2>
typename Arrow2::range compose (const typename Arrow1::domain& x, const Arrow2& arrow2, const Arrow1& arrow1) {
    return arrow2(arrow1(x));
}

template <typename Arrow, typename... Rest>
auto compose (const typename LastType<Rest...>::type::domain& x, const Arrow& arrow, const Rest&... rest) {
    return arrow(compose(x, rest...));
}

int main() {
    Arrow<std::string, int> f([](const std::string& s) {return s.length();});
    Arrow<int, double> g([](int x) {return x + 0.5;});
    Arrow<double, int> h([](double d) {return int(d+1);});
    std::cout << compose("hello", g, f) << '\n';  // 5.5
    std::cout << compose("hello", h, g, f) << '\n';  // 6
}

好的,谢谢你的回答。我已经在一段时间前就想到了这个设计。 - user3144405

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