返回转换容器的std :: transform-like函数

时间:2014-05-26 13:53:05

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

我试图实现一个类似于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是类模板,因此我的函数模板必须采用模板模板参数。此外,集合和矢量模板具有需要适当调整的不同数量的类型参数。

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

5 个答案:

答案 0 :(得分:36)

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

对于输入类型与输出类型相匹配的简单情况(我已经实现的不是你要问的那个)更高一级。而不是指定容器使用的类型T,并尝试专注于vector<T>等,只需指定容器本身的类型:

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=intS=std::string解决并处理集时,它会尝试实例化std::set<std::string, std::less<int>, ...>,从而无法实现#39; t compile。

要解决此问题,我们需要使用一组任意参数并将T的实例替换为U,即使它们是其他模板参数的参数。因此std::set<int, std::less<int>>应该变为std::set<std::string, std::less<std::string>>,依此类推。这包括一些自定义模板元编程,如其他答案所示。

拯救模板元编程

让我们创建一个模板,将其命名为replace_type,然后将T转换为U,将K<T>转换为K<U>。首先让我们处理一般情况。如果它不是模板类型,并且它与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<MyCustomType>转换为Container<SomethingElse>,那么可能会很好。但是,从Container<builtin_type>转换为Container<SomethingElse>时,另一个模板参数不应从builtin_type转换为SomethingElse,这似乎是合理的。此外,像std::mapstd::array这样的替代容器会给聚会带来更多问题。

处理std::mapstd::unordered_map并不是太糟糕。主要问题是replace_type需要替换更多类型。不仅有T - &gt; U替换,但也是std::pair<T, T2> - &gt; std::pair<U, U2>替换。这增加了对不需要的类型替换的关注程度,因为在飞行中不止一种类型。那就是说,这就是我发现的工作;请注意,在测试中我需要指定转换我的地图对的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


完全披露:虽然这种方法受到阿里引用Kerrek SB答案的影响,但我没有设法让它在Visual Studio 2013中运行,所以我建立了上述替代方案我。非常感谢部分Kerrek SB's original answer仍然是必要的,以及来自Constructor和Jarod42的刺激和鼓励。

答案 1 :(得分:8)

一些评论

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

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

    • std::vectorstd::dequestd::liststd::forward_list,[std::valarray]
    • std::queuestd::priority_queuestd::stack
    • std::setstd::unordered_set
  2. 具有两个非默认模板类型参数的容器(键的类型和值的类型):

    • std::mapstd::multi_mapstd::unordered_mapstd::unordered_multimap
  3. 具有两个非默认参数的容器:类型参数(值类型)和非类型参数(大小):

    • std::array
  4. 实施

    convert_container helper类将已知输入容器类型(InputContainer)和输出值类型(OutputType)的类型转换为输出容器的类型(typename convert_container<InputContainer, Output>::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<int> -> std::vector<std::string>
    • std::set<int> -> std::set<double>
    • std::map<int, char> -> std::map<char, int>

    问题

    std::array<int, 3> -> std::array<double, 3>转换无法编译,因为std::array没有insert所需的std::inserter方法。 transform_container函数不应该因此而使用以下容器:std::forward_liststd::queuestd::priority_queuestd::stack,[std::valarray] 。

答案 2 :(得分:6)

一般这样做会非常困难。

首先,考虑std::vector<T, Allocator=std::allocator<T>>,让我们说你的函子变换T->U。我们不仅需要映射第一个类型的参数,而且我们真的应该使用Allocator<T>::rebind<U>来获得第二个参数。这意味着我们需要首先知道第二个参数是一个分配器......或者我们需要一些机制来检查它是否有rebind成员模板并使用它。

接下来,考虑std::array<T, N>。在这里,我们需要知道第二个参数应该从字面上复制到我们的std::array<U, N>。也许我们可以不加改变地使用非类型参数,重新绑定具有重新绑定成员模板的类型参数,并用T替换文字U

现在,std::map<Key, T, Compare=std::less<Key>, Allocator=std::allocator<std::pair<Key,T>>>。我们应该Key不做更改,将T替换为U,不加更改Compare并将Allocator重新绑定到std::allocator<std::pair<Key, U>>。这有点复杂。

所以......你能没有任何灵活性吗?你是否乐于忽略关联容器并假设默认的分配器对你的转换输出容器是好的?

答案 3 :(得分:4)

主要困难是以某种方式从Container获取容器类型Conainer<T>。我无耻地窃取了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,这段代码有很多注意事项:&#34; ...你的重新绑定应该替换所有的参数,还是仅仅是第一个?不确定。是否应该在以后的参数中以T0递归替换T1?即std::map<T0, std::less<T0>> - &gt; std::map<T1, std::less<T1>>?&#34; 我还看到了上述代码的陷阱(例如,如何处理不同的分配器,另请参阅Useless' answer)。

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


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

答案 4 :(得分:0)

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

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也将详细介绍。