【问题标题】:The std::transform-like function that returns transformed container返回转换后容器的类 std::transform 函数
【发布时间】:2014-07-15 07:46:09
【问题描述】:

我正在尝试实现一个类似于std::transform 算法的函数,但不是通过参数获取输出迭代器,而是我想创建并返回一个包含转换后的输入元素的容器。

假设它被命名为transform_container 并接受两个参数:容器和函子。它应该返回相同的容器类型,但可能由不同的元素类型参数化(Functor 可以返回不同类型的元素)。

我想使用我的函数,如下例所示:

std::vector<int> vi{ 1, 2, 3, 4, 5 };
auto vs = transform_container(vi, [] (int i) { return std::to_string(i); }); 
//vs will be std::vector<std::string>
assert(vs == std::vector<std::string>({"1", "2", "3", "4", "5"}));

std::set<int> si{ 5, 10, 15 };
auto sd = transform_container(si, [] (int i) { return i / 2.; }); 
//sd will be of type std::set<double>
assert(sd == std::set<double>({5/2., 10/2., 15/2.}));

我能够编写两个函数——一个用于std::set,一个用于std::vector——似乎工作正常。除了容器类型名之外,它们是相同的。他们的代码如下所示。

template<typename T, typename Functor>
auto transform_container(const std::vector<T> &v, Functor &&f) -> std::vector<decltype(f(*v.begin()))>
{
    std::vector<decltype(f(*v.begin()))> ret;
    std::transform(std::begin(v), std::end(v), std::inserter(ret, ret.end()), f);
    return ret;
}

template<typename T, typename Functor>
auto transform_container(const std::set<T> &v, Functor &&f) -> std::set<decltype(f(*v.begin()))>
{
    std::set<decltype(f(*v.begin()))> ret;
    std::transform(std::begin(v), std::end(v), std::inserter(ret, ret.end()), f);
    return ret;
}

但是,当我尝试将它们合并为一个适用于任何容器的通用函数时,我遇到了许多问题。 setvector 是类模板,所以我的函数模板必须带一个模板模板参数。此外,set 和 vector 模板的类型参数数量不同,需要适当调整。

将上述两个函数模板泛化为可用于任何兼容容器类型的函数的最佳方法是什么?

【问题讨论】:

  • 使用可变参数模板。
  • @Constructor 好吧,我试过无济于事。也许我错过了什么或者被语法欺骗了——如果你知道怎么做,请告诉我怎么做。 :) 我可以接受一个模板模板参数,该参数由参数的可变数量参数化,但我不知道如何表达返回的类型。通常隐藏的默认容器参数(如分配器)需要明确表达并适当调整——我不知道如何以通用方式做到这一点。
  • 是的,我现在明白了。我会考虑你的问题。
  • 不是返回一个修改过的类型的容器(正如您将在下面指出的那样,这不能合理地完成),而是返回一个容器构建器,当它转换为容器时,它会构建它?它可以防止auto x = blah(),但std::vector&lt;double&gt; y = blah() 有效。
  • @Yakk 不必明确说明生成的容器类型是我想编写transform_container 函数的原因之一。这样我不仅可以节省打字时间,而且还可以将生成的容器作为参数传递给函数调用(并且可能对其进行模板参数推导)。您建议的方法有其自身的优点,但我更喜欢下面提出的解决方案——合理与否,它们提供了我正在寻找的功能。 :)

标签: c++ templates c++11 stl


【解决方案1】:

最简单的情况:匹配容器类型

对于输入类型与输出类型匹配的简单情况(我已经意识到这不是您要问的),上一级。无需指定容器使用的类型T,并尝试专门处理vector&lt;T&gt; 等,只需指定容器本身的类型即可:

template <typename Container, typename Functor>
Container transform_container(const Container& c, Functor &&f)
{
    Container ret;
    std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
    return ret;
}

更复杂:兼容的值类型

由于您想尝试更改容器存储的项目类型,您需要使用模板模板参数,并将T 修改为返回容器使用的。

template <
    template <typename T, typename... Ts> class Container,
    typename Functor,
    typename T, // <-- This is the one we'll override in the return container
    typename U = std::result_of<Functor(T)>::type,
    typename... Ts
>
Container<U, Ts...> transform_container(const Container<T, Ts...>& c, Functor &&f)
{
    Container<U, Ts...> ret;
    std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
    return ret;
}

什么是不兼容的值类型?

这只会让我们走到一半。它适用于从signedunsigned 的转换,但是当使用T=intU=std::string 解析并处理集合时,它会尝试实例化std::set&lt;std::string, std::less&lt;int&gt;, ...&gt;,因此不会编译。

为了解决这个问题,我们希望采用任意一组参数并将T 的实例替换为U,即使它们是其他模板参数的参数。因此std::set&lt;int, std::less&lt;int&gt;&gt; 应该变成std::set&lt;std::string, std::less&lt;std::string&gt;&gt;,以此类推。正如其他答案所建议的那样,这涉及一些自定义模板元编程。

模板元编程来拯救

让我们创建一个模板,将其命名为 replace_type,然后将其将 T 转换为 U,并将 K&lt;T&gt; 转换为 K&lt;U&gt;。首先让我们处理一般情况。如果不是模板类型,并且不匹配T,则其类型应保持K

template <typename K, typename ...>
struct replace_type { using type = K; };

然后是专业。如果不是模板类型,并且匹配T,则其类型应为U

template <typename T, typename U>
struct replace_type<T, T, U> { using type = U; };

最后是处理模板类型参数的递归步骤。对于模板化类型参数中的每种类型,相应地替换类型:

template <template <typename... Ks> class K, typename T, typename U, typename... Ks>
struct replace_type<K<Ks...>, T, U> 
{
    using type = K<typename replace_type<Ks, T, U>::type ...>;
};

最后更新transform_container 以使用replace_type

template <
    template <typename T, typename... Ts> class Container,
    typename Functor,
    typename T,
    typename U = typename std::result_of<Functor(T)>::type,
    typename... Ts,
    typename Result = typename replace_type<Container<T, Ts...>, T, U>::type
>
Result transform_container(const Container<T, Ts...>& c, Functor &&f)
{
    Result ret;
    std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
    return ret;
}

完成了吗?

这种方法的问题在于它不一定安全。如果您从Container&lt;MyCustomType&gt; 转换为Container&lt;SomethingElse&gt;,那可能没问题。但是当从Container&lt;builtin_type&gt; 转换为Container&lt;SomethingElse&gt; 时,另一个模板参数不应该从builtin_type 转换为SomethingElse 是合理的。此外,像std::mapstd::array 这样的替代容器会给聚会带来更多问题。

处理std::mapstd::unordered_map 还不错。主要问题是replace_type 需要替换更多类型。不仅有T -> U 替换,还有std::pair&lt;T, T2&gt; -> std::pair&lt;U, U2&gt; 替换。这增加了对不需要的类型替换的关注程度,因为飞行中的类型不止一种。也就是说,这就是我发现的工作;请注意,在测试中,我需要指定转换地图对的 lambda 函数的返回类型:

// map-like classes are harder. You have to replace both the key and the key-value pair types
// Give a base case replacing a pair type to resolve ambiguities introduced below
template <typename T1, typename T2, typename U1, typename U2>
struct replace_type<std::pair<T1, T2>, std::pair<T1, T2>, std::pair<U1, U2>>
{
    using type = std::pair<U1, U2>;
};

// Now the extended case that replaces T1->U1 and pair<T1,T2> -> pair<T2,U2>
template <template <typename...> class K, typename T1, typename T2, typename U1, typename U2, typename... Ks>
struct replace_type<K<T1, T2, Ks...>, std::pair<const T1, T2>, std::pair<const U1, U2>>
{
    using type = K<U1, U2, 
        typename replace_type< 
            typename replace_type<Ks, T1, U1>::type,
            std::pair<const T1, T2>,
            std::pair<const U1, U2>
        >::type ...
    >;
};

std::array 呢?

处理std::array 增加了痛苦,因为它的模板参数不能在上面的模板中推导出来。正如 Jarod42 所指出的,这是由于它的参数包括值而不仅仅是类型。我已经通过添加专业化和引入帮助器 contained_type 来为我提取 T (旁注,每个构造函数,这最好写成更简单的 typename Container::value_type 并适用于我在这里讨论过的所有类型) .即使没有 std::array 专业化,这也允许我将 transform_container 模板简化为以下内容(即使不支持 std::array,这也可能是一个胜利):

template <typename T, size_t N, typename U>
struct replace_type<std::array<T, N>, T, U> { using type = std::array<U, N>; };

// contained_type<C>::type is T when C is vector<T, ...>, set<T, ...>, or std::array<T, N>.
// This is better written as typename C::value_type, but may be necessary for bad containers
template <typename T, typename...>
struct contained_type { };

template <template <typename ... Cs> class C, typename T, typename... Ts>
struct contained_type<C<T, Ts...>> { using type = T; };

template <typename T, size_t N>
struct contained_type<std::array<T, N>> { using type = T; };

template <
    typename Container,
    typename Functor,
    typename T = typename contained_type<Container>::type,
    typename U = typename std::result_of<Functor(T)>::type,
    typename Result = typename replace_type<Container, T, U>::type
>
Result transform_container(const Container& c, Functor &&f)
{
    // as above
}

但是,transform_container 的当前实现使用 std::inserter,它不适用于 std::array。虽然可以进行更多专业化,但我将把它作为模板汤练习留给感兴趣的读者。在大多数情况下,我个人会选择不支持std::array

View the cumulative live example


完全披露:虽然这种方法受到 Ali 引用 Kerrek SB 答案的影响,但我没有设法让它在 Visual Studio 2013 中工作,所以我自己构建了上述替代方案。非常感谢Kerrek SB's original answer的部分内容仍然需要,以及来自Constructor和Jarod42的推动和鼓励。

【讨论】:

  • transform_container 可以返回一个由另一种类型参数化的容器。是当前问题的关键。
  • 问题中有一个使用transform_container函数的例子。 std::vector&lt;int&gt; 在其中转换为std::vector&lt;std::string&gt;std::set&lt;int&gt; 转换为std::set&lt;double&gt;
  • @Constructor 啊,我完全错过了那部分;我洞悉了 set 和 vector 之间的区别,而不是 int 和 double。我再去看看。
  • 而且还不完整:它不像std::array&lt;T, N&gt; ^_^那样处理容器。
  • 5 不是类型而是值。
【解决方案2】:

一些备注

以下方法允许从标准库转换任何类型的容器(std::array 存在问题,见下文)。对容器的唯一要求是它应该使用默认的std::allocator 类、std::lessstd::equal_tostd::hash 函数对象。所以我们有来自标准库的 3 组容器:

  1. 具有一个非默认模板类型参数(值类型)的容器:

    • std::vector, std::deque, std::list, std::forward_list, [std::valarray]
    • std::queue, std::priority_queue, std::stack
    • std::set, std::unordered_set
  2. 具有两个非默认模板类型参数(键类型和值类型)的容器:

    • std::map, std::multi_map, std::unordered_map, std::unordered_multimap
  3. 具有两个非默认参数的容器:类型参数(值的类型)和非类型参数(大小):

    • std::array

实施

convert_containerhelper 类将已知输入容器类型(InputContainer)和输出值类型(OutputType)的类型转换为输出容器类型(typename convert_container&lt;InputContainer, Output&gt;::type):

template <class InputContainer, class OutputType>
struct convert_container;

// conversion for the first group of standard containers
template <template <class...> class C, class IT, class OT>
struct convert_container<C<IT>, OT>
{
    using type = C<OT>;
};

// conversion for the second group of standard containers
template <template <class...> class C, class IK, class IT, class OK, class OT>
struct convert_container<C<IK, IT>, std::pair<OK, OT>>
{
    using type = C<OK, OT>;
};

// conversion for the third group of standard containers
template
    <
        template <class, std::size_t> class C, std::size_t N, class IT, class OT
    >
struct convert_container<C<IT, N>, OT>
{
    using type = C<OT, N>;
};

template <typename C, typename T>
using convert_container_t = typename convert_container<C, T>::type;

transform_container函数实现:

template
    <
        class InputContainer,
        class Functor,
        class InputType = typename InputContainer::value_type,
        class OutputType = typename std::result_of<Functor(InputType)>::type,
        class OutputContainer = convert_container_t<InputContainer, OutputType>
    >
OutputContainer transform_container(const InputContainer& ic, Functor f)
{
    OutputContainer oc;

    std::transform(std::begin(ic), std::end(ic), std::inserter(oc, oc.end()), f);

    return oc;
}

使用示例

查看live example 进行以下转换:

  • std::vector&lt;int&gt; -&gt; std::vector&lt;std::string&gt;,
  • std::set&lt;int&gt; -&gt; std::set&lt;double&gt;,
  • std::map&lt;int, char&gt; -&gt; std::map&lt;char, int&gt;

问题

std::array&lt;int, 3&gt; -&gt; std::array&lt;double, 3&gt; 转换无法编译,因为 std::array 没有 insert 方法,这是由于 std::inserter 而需要的)。出于这个原因,transform_container 函数不应该也适用于以下容器:std::forward_liststd::queuestd::priority_queuestd::stack、[std::valarray]。

【讨论】:

  • 谢谢!不支持array 没关系,和其他容器有很大区别。但是,我无法使用 VS 2013 编译您的代码,它显示为 could not deduce template argument for 'OutputContainer'。可能是编译器错误,他们对 C++11 的支持很古怪——我明天会仔细研究一下。
  • 天哪,我真的只是为了得到Container::value_type而实现了很长的路要走吗?当你只有一个模板时......
  • @MichałW.Urbańczyk 看起来像是 vc++ 中的一个错误。
  • @MichaelUrman 如我所见,是的。 :-) 但它不够长也不够有趣,不是吗?
  • 我很高兴我做到了,如果下次如此,也许我会记住简单的方法。但是您的帖子显示我还没有完成;我仍在试图弄清楚如何用我的方法处理std::mapstd::unordered_map。啊,你的处理它只指定前两个模板参数,所以默认分配器等等!
【解决方案3】:

一般来说这样做会非常困难。

首先,考虑std::vector&lt;T, Allocator=std::allocator&lt;T&gt;&gt;,假设您的函子变换T-&gt;U。我们不仅必须映射第一个类型参数,而且实际上我们应该使用Allocator&lt;T&gt;::rebind&lt;U&gt; 来获取第二个。这意味着我们首先需要知道第二个参数是一个分配器......或者我们需要一些机器来检查它是否有一个rebind 成员模板并使用它。

接下来,考虑std::array&lt;T, N&gt;。在这里,我们需要知道第二个参数应该按字面意思复制到我们的std::array&lt;U, N&gt;。或许我们可以直接采用非类型参数,重新绑定具有重新绑定成员模板的类型参数,并将文字T 替换为U

现在,std::map&lt;Key, T, Compare=std::less&lt;Key&gt;, Allocator=std::allocator&lt;std::pair&lt;Key,T&gt;&gt;&gt;。我们应该将Key 原封不动地取走,将T 替换为U,将Compare 原样取走并将Allocator 重新绑定到std::allocator&lt;std::pair&lt;Key, U&gt;&gt;。这有点复杂。

那么……没有这种灵活性,你能活下去吗?您是否乐于忽略关联容器并假设默认分配器适用于转换后的输出容器?

【讨论】:

    【解决方案4】:

    主要的困难是从Conainer&lt;T&gt; 获取容器类型Container。我从template metaprogramming: (trait for?) dissecting a specified template into types T<T2,T3 N,T4, ...>,尤其是Kerrek SB's answer(公认的答案)中无耻地窃取了代码,因为我不熟悉模板元编程。

    #include <algorithm>
    #include <cassert>
    #include <type_traits>
    
    // stolen from Kerrek SB's answer
    template <typename T, typename ...>
    struct tmpl_rebind {
        typedef T type;
    };
    
    template <template <typename ...> class Tmpl, typename ...T, typename ...Args>
    struct tmpl_rebind<Tmpl<T...>, Args...> {
        typedef Tmpl<Args...> type;
    };
    // end of stolen code
    
    template <typename Container,
              typename Func,
              typename TargetType = typename std::result_of<Func(typename Container::value_type)>::type,
              typename NewContainer = typename tmpl_rebind<Container, TargetType>::type >
    NewContainer convert(const Container& c, Func f) {
    
        NewContainer nc;
    
        std::transform(std::begin(c), std::end(c), std::inserter(nc, std::end(nc)), f);
    
        return nc;
    }
    
    int main() {
    
        std::vector<int> vi{ 1, 2, 3, 4, 5 };
        auto vs = convert(vi, [] (int i) { return std::to_string(i); });
        assert( vs == std::vector<std::string>( {"1", "2", "3", "4", "5"} ) );
    
        return 0;
    }
    

    我已经用 gcc 4.7.2 和 clang 3.5 测试了这段代码,并按预期工作。

    As Yakk points out,虽然这段代码有很多警告:"...你的重新绑定应该替换所有参数,还是只替换第一个参数?不确定。它是否应该递归替换 T0 与 @987654329 @ 在后面的参数中?即std::map&lt;T0, std::less&lt;T0&gt;&gt; -> std::map&lt;T1, std::less&lt;T1&gt;&gt;?” 我还看到了上面代码的陷阱(例如,如何处理不同的分配器,另见Useless' answer)。

    尽管如此,我相信上面的代码对于简单的用例已经很有用了。如果我们正在编写一个实用函数以提交给 boost,那么我会更有动力进一步研究这些问题。但是已经有一个公认的答案,所以我认为这个案子已经结案了。


    非常感谢 Constructor、dyp 和 Yakk 指出我的错误/错失的改进机会。

    【讨论】:

    • 为什么要显式设置NewType 模板参数,而不是像OP 在他的回答中那样使用仿函数获取它?
    • @Constructor 是的,谢谢,我已经解决了这个问题。顺便说一句,std::result_of 提供了一种更易读的方式来实现相同的目标。
    • std::inserter(nc, std::end(nc)) 为什么不是back_inserter
    • 添加一些template&lt;class T&gt;using foo_t=typename foo&lt;T&gt;::type 以减少返回类型的噪音(从typename foo&lt;T&gt;::typefoo_t&lt;T&gt;
    • 第二,你的重新绑定应该替换所有参数,还是只替换第一个?不确定。是否应该在后面的参数中递归地用T1 替换T0?即std::map&lt;T0, std::less&lt;T0&gt;&gt; -> std::map&lt;T1, std::less&lt;T1&gt;&gt;?嗯
    【解决方案5】:

    我最近写了一篇博客文章来解决类似的问题。使用模板和迭代器接口是我选择遵循的路线。

    for_each:

    为了减少样板的数量,我们将创建一个using 子句,允许我们获取迭代器中包含的类型:

    template <typename IteratorType>
    using ItemType = typename std::iterator_traits<typename IteratorType::iterator>::value_type;
    

    有了它,我们可以像这样实现一个辅助函数for_each

    template <typename IteratorType>
    void for_each(IteratorType &items, std::function<void(ItemType<IteratorType> const &item)> forEachCb)
    {
        for (typename IteratorType::iterator ptr = items.begin(); ptr != items.end(); ++ptr)
            forEachCb(*ptr);
    }
    

    transform_container:

    终于transform_container,可以这样实现:

    template <typename IteratorType, typename ReturnType>
    ReturnType transform_container(IteratorType &items, std::function<ItemType<ReturnType>(ItemType<IteratorType> const &item)> mapCb)
    {
        ReturnType mappedIterator;
        for_each<IteratorType>(items, [&mappedIterator, &mapCb](auto &item) { mappedIterator.insert(mappedIterator.end(), mapCb(item)); });
        return mappedIterator;
    }
    

    这将允许我们以下列方式调用您的两个示例:

    std::vector<int> vi{ 1, 2, 3, 4, 5 };
    auto vs = transform_container<std::vector<int>, std::vector<std::string>>(vi, [](int i){return std::to_string(i);});
    assert(vs == std::vector<std::string>({"1", "2", "3", "4", "5"}));
    
    std::set<int> si{ 5, 10, 15 };
    auto sd = transform_container<std::set<int>, std::set<double>>(si, [] (int i) { return i / 2.; }); 
    assert(sd == std::set<double>({5/2., 10/2., 15/2.}));
    

    如果有帮助,我的blog post 也会详细介绍。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多