【问题标题】:Equivalent of python map function using lambda等效于使用 lambda 的 python 映射函数
【发布时间】:2016-01-27 12:34:07
【问题描述】:

我想知道是否可以使用自动返回类型推导功能编写 Python 函数 map 的 C++ 等效项。我的想法是这样的:

vector<int> input({1,2,3});
auto output=apply(input,[](int num){return num*num;});

//output should be a vector {1,4,9}

我确实知道std::transform,但在目前的情况下,编写范围循环似乎更容易。

【问题讨论】:

  • 使用 std::transform 将是可行的方法 - 您认为此解决方案有什么问题?我认为它非常简洁。
  • 循环不需要更多的输入,对吧?我觉得循环更具可读性(有些人同意我的观点,似乎stackoverflow.com/questions/3885317/…)。
  • 我认为打字会少一些。我想可读性是个人喜好 - 我个人只会对这样的简单案例使用循环。
  • 我也一直认为 std::transform() 不如 Python 的 map() 强大,但实际上它更强大,因为您可以决定 std::transform() 应该写入的任何 output_iterator。这意味着您可以非常快速地从例如打印 (ostream_iterator&lt;&gt;(cout)) 以使用 insert_iterator&lt;&gt;(set, set.begin()) 聚合一组唯一值(当然语法正确,但你明白了)
  • 我同意,但有时,我只需要 python map()

标签: c++ templates lambda c++14


【解决方案1】:

这当然可以完成,并且可能看起来像这样:

template <class Container, class Function>
auto apply (const Container &cont, Function fun) {
    std::vector< typename
            std::result_of<Function(const typename Container::value_type&)>::type> ret;
    ret.reserve(cont.size());
    for (const auto &v : cont) {
        ret.push_back(fun(v));
    }
    return ret;
}

如果您想要超级通用并处理 C 数组和所有内容,您可能需要为特殊情况添加几个重载。

Live example

【讨论】:

    【解决方案2】:

    Baum mit Augen 的答案大部分都在那里。只需再执行几个步骤即可支持任何适用于每个人的功能:

    template <typename C, typename F>
    auto apply(C&& container, F&& func)
    {
        using std::begin;
        using std::end;
    
        using E = std::decay_t<decltype(std::forward<F>(func)(
            *begin(std::forward<C>(container))))>;
    
        std::vector<E> result;
        auto first = begin(std::forward<C>(container));
        auto last = end(std::forward<C>(container));
    
        result.reserve(std::distance(first, last));
        for (; first != last; ++first) {
            result.push_back(std::forward<F>(func)(*first));
        }
        return result;
    }
    

    我们甚至可以更进一步,通过不使用 C++14 auto 推演,而是将故障转移到推演阶段,使这个 SFINAE 成为可能。从begin/end 的助手开始:

    namespace adl_helper {
        using std::begin;
        using std::end;
    
        template <typename C>
        auto adl_begin(C&& c) -> decltype(begin(std::forward<C>(c))) {
            return begin(std::forward<C>(c));
        }
    
        template <typename C>
        auto adl_end(C&& c) -> decltype(end(std::forward<C>(c))) {
            return end(std::forward<C>(c));
        }    
    }
    
    using adl_helper::adl_begin;
    using adl_helper::adl_end;
    

    然后用它来推导出之前的E

    using adl_helper::adl_begin;
    using adl_helper::adl_end;
    
    template <typename C,
              typename F,
              typename E = std::decay_t<decltype(std::declval<F>()(
                  *adl_begin(std::declval<C>())
                  ))>
               >
    std::vector<E> apply(C&& container, F&& func)
    {
        /* mostly same as before, except using adl_begin/end instead
           of unqualified begin/end with using
        */
    }
    

    现在我们可以在编译时测试某个容器/函数对是否为apply-able,错误是推断失败而不是使用失败:

    int arr[] = {1, 2, 3};
    auto x = apply(arr, []{ return 'A'; });
    
    main.cpp: In function 'int main()':
    main.cpp:45:52: error: no matching function for call to 'apply(int [3], main()::<lambda()>)'
        auto x = apply(arr, []() -> char { return 'A'; });
                                                        ^
    main.cpp:29:16: note: candidate: template<class C, class F, class E> std::vector<E> apply(C&&, F&&)
     std::vector<E> apply(C&& container, F&& func)
                    ^
    main.cpp:29:16: note:   template argument deduction/substitution failed:
    main.cpp:25:50: error: no match for call to '(main()::<lambda()>) (int&)'
               typename E = decltype(std::declval<F>()(
                                                      ^
    

    正如所指出的,这不能很好地处理输入迭代器的容器。所以让我们修复它。我们需要一些东西来确定容器的大小。如果容器有size() 成员函数,我们可以使用它。否则,如果迭代器没有类别input_iterator_tag(不知道任何其他区分输入迭代器的方法......),我们可以使用它。否则,我们有点倒霉。像这样进行优先顺序递减的一个好方法是引入chooser 层次结构:

    namespace details {
        template <int I> struct chooser : chooser<I-1> { };
        template <> struct chooser<0> { };
    }
    

    然后就往下走:

    namespace details {
        template <typename C>
        auto size(C& container, chooser<2>) -> decltype(container.size(), void())
        {
            return container.size();
        }
    
        template <typename C,
                  typename It = decltype(adl_begin(std::declval<C&>()))
                  >
        auto size(C& container, chooser<1>) 
        -> std::enable_if_t<
            !std::is_same<std::input_iterator_tag,
                typename std::iterator_traits<It>::iterator_category
            >::value,
            size_t>
        {
            return std::distance(adl_begin(container), adl_end(container));
        }
    
        template <typename C>
        size_t size(C& container, chooser<0>)
        {
            return 1; // well, we have no idea
        }
    }
    
    template <typename C>
    size_t size(C& container)
    {
        return size(container, details::chooser<10>{});
    }
    

    然后我们可以尽我们所能使用size()reserve() 我们的向量:

    template <typename C,
              typename F,
              typename E = std::decay_t<decltype(std::declval<F>()(
                  *adl_begin(std::declval<C>())
                  ))>
               >
    std::vector<E> apply(C&& container, F&& func)
    {
        std::vector<E> result;
        result.reserve(size(container));
    
        for (auto&& elem : container) {
            result.push_back(std::forward<F>(func)(std::forward<decltype(elem)>(elem)));
        }
        return result;
    }
    

    【讨论】:

    • 简洁,很好地概括了我的回答; +1。请问:为什么函数参数的完美转发?标准库(当然,它并不完美)按值传递。有显着的好处吗?
    • 顺便说一句,"anything that is for-each-able" 并不完全正确,因为std::for_each 支持输入迭代器,result.reserve(std::distance(first, last)); 会破坏这些东西。 (在你问之前:我不会提供一个有意义的例子,我只是在迂腐。)
    • @BaummitAugen 输入迭代器已处理 :)
    • 如何使用这个?前面的示例只返回一个常量。我在想这样的事情:apply(intValues, [](elem) { return elem*2; });
    • @UserOneFourTwo 前面的示例返回一个常量是什么?
    【解决方案3】:

    这适用于您的示例以及大多数容器。我使用 std::transform,因为它可以针对每个 stl 迭代器进行优化。我是从 Baum mit Augen 的回答开始的,后来被删了。

    template<typename Container, typename Function>
    using _mapT = std::vector<typename std::result_of<Function(const typename Container::value_type&)>::type>;
    
    template <typename Container, typename Function>
    _mapT<Container, Function> map(const Container &container, Function &&f)
    {
        _mapT<Container, Function> ret; ret.reserve(container.size());
        std::transform(container.begin(), container.end(), std::back_inserter(ret), std::forward<Function>(f));
        return ret;
    }
    

    【讨论】:

    • 这将默认构造或值初始化未知数量的任意复杂类型,然后再重新分配它们。这不是一个好主意。如果你真的想在这里使用std::transform(这很好),请查看std::back_inserter)。
    • @BaummitAugen 我在发布之前运行了我的版本,所以这个版本适用于 msvc2015。 back_inserter 确实好多了:)。我删除了 C++14 功能,这应该与 C++11 一起运行。
    • 这几乎没问题,但_mapT 是全局范围内的实现保留标识符,因此仅在包装在不同范围内时才合法。我也不明白为什么你首先要引入一个新名称,即使你想使用 C++11,你也可以使用尾随返回类型。
    • @user2308211 您可能混淆了reserveresizereserve 很好,如果你知道大小是个好主意,resize 是错误的。
    • @BaummitAugen 我知道我又含糊其辞了。它可以为每个容器的迭代器实现,并且由于其限制,编译器可以进行更多优化。
    【解决方案4】:

    这已经在 cmets 中讨论过了,但我认为它也应该作为答案给出: &lt;algorithm&gt; 中的函数 std::transform 可以满足您的需求。

    以下代码适用于我:

    #include <vector>
    #include <algorithm>
    using namespace std;
    
    //...
    
    vector<int> input({1,2,3});
    transform(input.begin(), input.end(), input.begin(), [](int num){return num*num;});
    

    【讨论】:

      猜你喜欢
      • 2016-08-13
      • 2010-10-30
      • 1970-01-01
      • 1970-01-01
      • 2020-12-19
      • 1970-01-01
      • 1970-01-01
      • 2018-02-21
      • 1970-01-01
      相关资源
      最近更新 更多