【问题标题】:Functional composition with variadic templates in C++11在 C++11 中使用可变参数模板进行功能组合
【发布时间】:2013-12-29 19:18:11
【问题描述】:

我是一名数学家,很长一段时间以来都习惯于进行“老式”C++ 编程。我觉得 C++11 提供的一些新语法结构可以帮助我在我的专业项目中获得更好的代码。然而,由于我不是 CS 专业人士,我必须承认我缺乏理解我在自学过程中遇到的一些例子的知识,尽管到目前为止我很幸运/成功。

我的印象是可变参数模板可用于实现类型安全的函数组合,如this question。我的担忧稍微笼统一些,因为我想用异构(但兼容)的参数/返回类型来组合函数。我搜索了很多并找到了another reference,但这对我来说似乎完全是“黑魔法”;)我不会假装我可以在我的上下文中调整代码,尽管我觉得我应该在那里找到我需要的东西.

我认为下面的(最不完整的)代码对于我想要实现的目标来说是相对不言自明的。特别是我相信正确的实现会在尝试编写不兼容的函数时引发编译时错误(此处为箭头),并且需要一段递归模板代码。

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 &lt; source_of_first_arrow , target_of_last_arrow&gt;
和方法
arrow()
换成对应的
eval()

但我觉得上面的代码更能说明问题。

任何帮助都将不胜感激,即使它包含粗暴的指责,并带有指向现有(相对基本)示例的指针,这肯定会逃脱我的搜索。谢谢!

【问题讨论】:

    标签: c++ c++11 functional-programming variadic-templates


    【解决方案1】:

    如果我没记错的话,你不需要花哨的模板魔法来做这个组合。这是几乎不言自明的代码:

    #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 函数”:是的,这就是让我害怕的地方 ;) 感谢您的(非常明确的)输入,我会尝试使其适应更复杂的情况要组合的任意数量的函数(因此是可变参数模板标签)。得到你提供的基本建筑砖应该不会太难。
    • 您可以使用此代码组合两个以上。只需执行auto f = f1 * f2 * f3 * f4;。或者您是否希望将其更像函数调用? (auto f = compose(f1, f2, f3, f4);)
    • 是的,我的意思是后者。我想要一些可以写成 compose(f1,...) [而且我似乎无法使用内联代码字符样式...?]
    • @LoïcTeyssier,这并不像我最初想象的那么简单。我将其附加到答案中。如果过程不清楚,请询问。
    • 是的,对我来说主要的困难是模板参数的递归,我想弄清楚。由于您的两种解决方案都具有不同的性质,因此它们对我都有启发性。谢谢你的回答,我明天试试这个并保持联系。
    【解决方案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
    }
    

    【讨论】:

    • 好的,谢谢您的回答。我已经在一段时间前完成了这个设计。
    猜你喜欢
    • 1970-01-01
    • 2016-06-27
    • 1970-01-01
    • 2018-02-20
    • 2012-11-27
    • 2021-05-02
    • 2012-05-13
    • 1970-01-01
    • 2015-11-10
    相关资源
    最近更新 更多