返回转换容器的std :: transform-like函数 [英] The std::transform-like function that returns transformed container

查看:159
本文介绍了返回转换容器的std :: transform-like函数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图实现类似 std :: transform 算法的函数,但不是通过一个参数取输出迭代器我想创建和返回一个容器变换的输入元素。



我们假设它的名字为 transform_container ,并且有两个参数:container和functor。它应该返回相同的容器类型,但可能由不同的元素类型参数化(Functor可以返回不同类型的元素)。



我想使用我的函数在下面的示例中:

  std :: vector< int& vi {1,2,3,4,5}; 
auto vs = transform_container(vi,[](int i){return std :: to_string(i);});
// vs将是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的类型为std :: set< double>
assert(sd == std :: set< double>({5/2。,10/2。,15/2。}));

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

 模板< 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 :: insertionter(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;
}

然而,当我试图将它们合并成一个通用函数任何容器,我遇到了许多问题。 set 向量是类模板,因此我的函数模板必须使用模板模板参数。此外,集合和向量模板具有需要适当调整的不同数量的类型参数。



将上述两个函数模板推广到与任何兼容容器类型一起使用的函数的最好方法是什么?

解决方案

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



对于输入类型与输出类型匹配ve sinced意识到不是你要问的)去一级高。而不是指定您的容器使用的类型 T ,并尝试专门处理向量< 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 :: insertionter(ret,std :: end(ret)),f);
return ret;
}



更复杂:兼容值类型



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

  template< 
template< typename T,typename ... Ts> class Container,
typename Functor,
typename T,//< - 这是我们将在返回容器中重写的一个
typename U = std :: result_of< Functor )> :: 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 :: insertionter(ret,std :: end(ret)),f);
return ret;
}



什么是不兼容的值类型?



这只会让我们在那里。它适用于从签名无符号的转换,但是当解析为 T = int S = std :: string 和处理集,它试图实例化 std :: set< std :: string ,std :: less< int> ;, ...> ,因此无法编译。



取任意参数集并用 U 替换 T 的实例,即使它们是其他模板参数的参数。因此 std :: set< int,std :: less< int>> 应成为 std :: set< std :: string,std: :less< std :: string>> ,等等。这包括一些自定义模板元程序设计,如其他答案所示。



模板元编程救援



创建模板,将其命名为 replace_type ,并将 T 转换为 U K K 。首先让我们处理一般情况。如果它不是模板类型,并且不匹配 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&类K,类型名称T,类型名称U,类型名称Ks> 
struct replace_type< K< Ks ...>,T,U>
{
using type = K< typename replace_type< Ks,T,U> :: type ...>
};

最后更新 transform_container 以使用 replace_type

  
template< typename T,typename ... Ts>类型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
>
结果transform_container(const Container< T,Ts ...>& c,Functor& f)
{
Result ret;
std :: transform(std :: begin(c),std :: end(c),std :: insertionter(ret,std :: end(ret)),f);
return ret;
}



这完成了吗?



这种方法的问题是它不一定安全。如果你从 Container< MyCustomType> 转换为 Container< SomethingElse> ,可能很好。但是当将 Container< builtin_type> 转换为 Container< SomethingElse> 时,不应转换另一个模板参数从 builtin_type SomethingElse 。此外, std :: map std :: array 的替代容器会给参与方带来更多问题。



处理 std :: map std :: unordered_map isn'太糟糕了。主要的问题是 replace_type 需要替换更多的类型。不仅有 T - > U 替换,还有一个 std :: pair< ; T,T2> - > std :: pair< U,U2> 这增加了对不需要的类型替换的关注程度,因为在飞行中存在多个类型。这就是说,这是我发现工作;注意,在测试中,我需要指定转换我的map对的lambda函数的返回类型:

  // map-like类更难。您必须替换键和键值对类型
//给一个基本案例替换对类型以解决以下引入的歧义
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> ;;
};

//现在替换T1-> U1并且对< T1,T2> - >对< T2,U2>
template< template< typename ...>类K,类型名T1,类型名T2,类型名U1,类型名U2,类型名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 ):

  ;类型名称T,size_t N,类型名称U> 
struct replace_type< std :: array< T,N>,T,U> {using type = std :: array< U,N> ;; };当C是向量< T,...>,set< T,...>或std :: array< T时,

// contained_type< C& N>。
//这更好地写为typename C :: value_type,但是对于坏容器可能是必要的
template< typename T,typename ...>
struct contained_type {};

template< template< typename ... Cs>类C,类型名T,类型名... 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 contains_type< Container> :: type,
typename U = typename std :: result_of< Functor(T)> ;: :type,
typename Result = typename replace_type< Container,T,U> :: type
>
结果transform_container(const Container& c,Functor& f)
{
//如上所述
}
然而, transform_container 的当前实现使用 std :: inserter 它不能使用 std :: array 。虽然有可能做更多的专业,我将把这作为模板汤练习感兴趣的读者。在大多数情况下,我个人会选择不支持 std :: array



查看累积实例






完全公开:虽然这种方法受到Ali的引用Kerrek SB的回答的影响,我没有设法让它在Visual Studio 2013中工作,所以我自己构建了上述替代。非常感谢 Kerrek SB的原始回答的部分,以及从构造函数和Jarod42的刺激和鼓励。


I'm trying to implement a function similar to std::transform algorithm but instead of taking the output iterator by an argument I want to create and return a container with transformed input elements.

Let's say that it's named transform_container and takes two arguments: container and functor. It should return the same container type but possibly parametrized by a different element type (the Functor can return element of different type).

I'd like to use my function as in the example below:

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.}));

I was able two write two functions — one for std::set and one for std::vector — that seem to work properly. They are identical, except of the container typename. Their code is listed below.

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;
}

However, when I attempted to merge them into a single general function that works with any container, I encountered numerous issues. The set and vector are class templates, so my function template must take a template template parameter. Moreover, set and vector templates have a different number of type parameters that needs to be properly adjusted.

What is the best way to generalize the two function templates above into a function that works with any compatible container type?

解决方案

Simplest cases: matching container types

For the simple case where the input type matches the output type (which I've sinced realized is not what you're asking about) go one level higher. Instead of specifying the type T that your container uses, and trying to specialize on a vector<T>, etc., just specify the type of the container itself:

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;
}

More complexity: compatible value types

Since you want to try to change the item type stored by the container, you'll need to use a template template parameter, and modify the T that the returned container uses.

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;
}

What of incompatible value types?

This only gets us partway there. It works fine with a transform from signed to unsigned but when resolved with T=int and S=std::string, and handling sets, it tries to instantiate std::set<std::string, std::less<int>, ...> and thus doesn't compile.

To fix this, we want to take an arbitrary set of parameters and replace instances of T with U, even if they are the parameters to other template parameters. Thus std::set<int, std::less<int>> should become std::set<std::string, std::less<std::string>>, and so forth. This involves some custom template meta programming, as suggested by other answers.

Template metaprogramming to the rescue

Let's create a template, name it replace_type, and have it convert T to U, and K<T> to K<U>. First let's handle the general case. If it's not a templated type, and it doesn't match T, its type shall remain K:

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

Then a specialization. If it's not a templated type, and it does match T, its type shall become U:

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

And finally a recursive step to handle parameters to templated types. For each type in a templated type's parameters, replace the types accordingly:

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 ...>;
};

And finally update transform_container to use 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;
}

Is this complete?

The problem with this approach is it is not necessarily safe. If you're converting from Container<MyCustomType> to Container<SomethingElse>, it's likely fine. But when converting from Container<builtin_type> to Container<SomethingElse> it's plausible that another template parameter shouldn't be converted from builtin_type to SomethingElse. Furthermore, alternate containers like std::map or std::array bring more problems to the party.

Handling std::map and std::unordered_map isn't too bad. The primary problem is that replace_type needs to replace more types. Not only is there a T -> U replacement, but also a std::pair<T, T2> -> std::pair<U, U2> replacement. This increases the level of concern for unwanted type replacements as there's more than a single type in flight. That said, here's what I found to work; note that in testing I needed to specify the return type of the lambda function that transformed my map's pairs:

// 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 ...
    >;
};

What about std::array?

Handling std::array adds to the pain, as its template parameters cannot be deduced in the template above. As Jarod42 notes, this is due to its parameters including values instead of just types. I've gotten partway by adding specializations and introducing a helper contained_type that extracts T for me (side note, per Constructor this is better written as the much simpler typename Container::value_type and works for all types I've discussed here). Even without the std::array specializations this allows me to simplify my transform_container template to the following (this may be a win even without support for 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
}

However the current implementation of transform_container uses std::inserter which does not work with std::array. While it's possible to make more specializations, I'm going to leave this as a template soup exercise for an interested reader. I would personally choose to live without support for std::array in most cases.

View the cumulative live example


Full disclosure: while this approach was influenced by Ali's quoting of Kerrek SB's answer, I didn't manage to get that to work in Visual Studio 2013, so I built the above alternative myself. Many thanks to parts of Kerrek SB's original answer are still necessary, as well as to prodding and encouragement from Constructor and Jarod42.

这篇关于返回转换容器的std :: transform-like函数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆