使用temp模拟返回类型的类型推导。代理与转换运算符

时间:2013-03-26 21:29:27

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

我有这个Haskell风格的map高阶函数,除了结果类型之外,它已经在调用者端使用模板类型推导来获得干净的语法:

template<class ResultingContainer, class R, class Fn>
ResultingContainer map(const R &range, const Fn &f) {
    ResultingContainer result;
    using value_type = decltype(*std::begin(range));
    for_each(range, [&](const value_type &v){
        result.push_back(f(v));
    });
    return result;
}

(我更喜欢decltype(*std::begin(range))超过R::value_type或某些迭代器特征,因为我使用的容器不提供这样的接口:Qt容器。)

使用此功能很简单,但并不像我希望的那样简单,你们都知道我的意思:

std::vector<int> result = map<std::vector<int>>(otherVector, someFunction);

当然,我希望它是:

std::vector<int> result = map(otherVector, someFunction);

如果我按以下方式设计API,那么类型演绎将完美无缺。但我不希望它像这样,因为这是C ++标准库样式(除了迭代器对/范围之间的差异),但我希望它是“Haskell风格”:

std::vector<int> result;
map(otherVector, result, someFunction);

鉴于otherVector不一定与结果的类型相同(例如,它们可能是字符串,我想要它们的长度,所以someFunction返回给定的长度字符串),我不能仅使用已知类型(特别是输入的值类型)来表达类型ResultingContainer

如果我(我不想)坚持使用单一类型的容器,我可以表达它。但是还有一个希望:编译器已经知道结果应该是什么,除非auto用于返回类型。 (当结果类型有多种可能性时,例如当用作重载函数的参数时,它会令人困惑。)

所以我想到了代理容器,这是默认的ResultingContainer(但如果用户需要可以覆盖,那不是我的问题)。但是,根据我目前的知识,我只能实现一些ProxyContainer<T>T是[{1}}的结果类型,f中使用的谓词),其中暂时保存所有结果项。这听起来像不必要的开销。但是,实现可能如下所示:

map

但这感觉不对。

你能想出更好的解决方案吗?

2 个答案:

答案 0 :(得分:2)

我使用&#34;懒惰适配器&#34;解决了这个问题。正如Xeo所说。但是我没有使用Boost,而是写了一个非常简单的:

template<class R, class Fn>
class MapAdaptor
{
    const R &range;
    const Fn &f;

public:
    MapAdaptor(const R &range, const Fn &f) :
        range(range), f(f)
    {
    }

    template<class ResultingContainer>
    operator ResultingContainer() const {
        ResultingContainer result;
        for(const auto &v : range)
            result.push_back(f(v));
        return result;
    }
};

然后,实际的map函数变为以下内容:

template<class R, class Fn>
MapAdaptor<R,Fn> map(const R &range, const Fn &f) {
    return MapAdaptor<R,Fn>(range, f);
}

我还没有将适配器作为一个范围,但我会添加此功能,以便能够进行链/嵌套操作。

答案 1 :(得分:1)

这是一个通用的延迟适配器实现,以及一些container_traits,允许您将数据返回到std::set等。

#include <tuple>
#include <iterator>
#include <utility>
#include <algorithm>

// Standard metaprogramming boilerplate:
template<std::size_t...>
struct seq {};
template<std::size_t max, std::size_t... s>
struct make_seq:make_seq<max-1, max-1, s...> {};
template<std::size_t... s>
struct make_seq<0, s...> {
  typedef seq<s...> type;
};
// helper to make creating a sequence from 0,...,max-1 take less typing:
template<std::size_t max>
using MakeSeq = typename make_seq<max>::type;

// takes a sequence of indexes, a tuple, a Result type, and a Functor, and calls the Functor passing
// in the Result type with the fowarded args in the tuple, as indicated by the sequence of indexes:
template<typename Result, template<typename Result>class Functor, typename... Args, std::size_t... s>
Result apply( seq<s...>, std::tuple<Args...>& args ) {
  return Functor<Result>()(std::forward<Args>(std::get<s>(args))...);
}

// and here it is, a generic lazy evaluator:
template<template<typename Result>class Functor, typename... Args>
struct LazyEvaluation {
  std::tuple<Args...> stored_args;
  LazyEvaluation( Args... args ):stored_args(std::forward<Args>(args)...) {};
  template<typename R>
  operator R() {
    return apply<R, Functor>( MakeSeq<sizeof...(Args)>(), stored_args );
  }
};

// The start of some container traits templates:
template<typename T, typename=void>
struct const_iterator_type:std::false_type {};
template<typename T>
struct const_iterator_type<T, typename std::enable_if<
  std::is_same< typename T::const_iterator, typename T::const_iterator >::value
>::type>:std::true_type {
   typedef typename T::const_iterator const_iterator;
};
template<typename T, size_t n>
struct const_iterator_type< T[n] >:std::true_type {
   typedef T const* const_iterator;
};


template<typename T,typename=void>
struct has_push_back:std::false_type {};
template<typename T>
struct has_push_back<T, typename std::enable_if<
  std::is_same<
     decltype(std::declval<T>().push_back(*std::declval<typename const_iterator_type<T>::const_iterator>())),
     decltype(std::declval<T>().push_back(*std::declval<typename const_iterator_type<T>::const_iterator>()))
  >::value
>::type>:std::true_type{};
template<typename T,typename=void>
struct has_insert:std::false_type {};
template<typename T>
struct has_insert<T, typename std::enable_if<
  std::is_same<
     decltype(std::declval<T>().insert(*std::declval<typename const_iterator_type<T>::const_iterator>())),
     decltype(std::declval<T>().insert(*std::declval<typename const_iterator_type<T>::const_iterator>()))
  >::value
>::type>:std::true_type {};

template<typename Container, typename=void>
struct container_traits;

template<typename Container>
struct container_traits<Container, typename std::enable_if<has_push_back<Container>::value>::type> {
   template<typename V>
   static void add_to_container( Container& c, V&& v ) {
     c.push_back( std::forward<V>(v) );
   }
};
template<typename Container>
struct container_traits<Container, typename std::enable_if<!has_push_back<Container>::value && has_insert<Container>::value>::type> {
   template<typename V>
   static void add_to_container( Container& c, V&& v ) {
     c.insert( std::forward<V>(v) );
   }
};

// supporting emplace_back and emplace is harder, but probably worth it.
// the trick with both of them is that you only know if they exist or are valid
// after you try to call add_to_container with the arguments!  So instead of
// "does it exist", you end up with "can we call emplace_back with these arguments".
// This requires a bit of slight of hand in the template code, as we want to fall back
// on insert and push_back if emplace_back doesn't exist.

// Another improvement to the above might be to have the const_iterator traits class
// fall back on a decltype( std::begin(std::declval<C const>()) ) -- or even better,
// do full ADL with a private namespace and a using std::begin.

// your code, basically verbatim, but uses container_traits to figure out
// how to add an element to the container:
template<class ResultingContainer, class Range, class Fn>
ResultingContainer map_now(const Range &range, const Fn &f) {
  ResultingContainer result;
  using value_type = decltype(*std::begin(range));
  for_each(std::begin(range), std::end(range), [&](const value_type &v){
    container_traits<ResultingContainer>::add_to_container(result, f(v));
  });
  return result;
}

// could make this easier if I could pass in a pointer-to-template-function
// or the equivalent directly.  Know the syntax by any chance?
template<typename Range, typename Fn>
struct map_lazy_helper {
  template<typename ResultingContainer>
  struct Func {
    ResultingContainer operator()(const Range &range, const Fn &f) const {
      return map_now<ResultingContainer>( range, f );
    }
  };
};

// Map lazy is mostly repeating type information:
template<typename Range, typename Fn>
LazyEvaluation<map_lazy_helper<Range, Fn>::template Func, Range, Fn>
map_lazy(Range&&range, Fn&&f) {
  return {std::forward<Range>(range), std::forward<Fn>(f)};
}

#include <iostream>
#include <vector>
#include <set>

int main() {
   std::vector<int> tester {3,2,1};

   std::vector<double> vd = map_lazy( tester, [](int x) { return x*0.5; } );
   std::set<double> sd = map_lazy( tester, [](int x) { return x*0.5; } );
   std::vector<int> vs = map_lazy( tester, [](int x) { return x*2; } );
   for(auto&& x:vd)
     std::cout << x << "\n";
   for(auto&& x:sd)
     std::cout << x << "\n";
   for(auto&& x:vs)
     std::cout << x << "\n";
}

可以和你喜欢的一样工作。 :)

有趣的是,虽然这个答案很大,但其中40%是处理set的通用“添加容器”代码,10%是元编程样板,30%是通用LazyEvaluation,10 %是测试代码,只有10%是实际编写map_lazy所需的代码。

现在我考虑一下,你想要的是一个通用的“插入容器”迭代器,所以你可以支持写入足够大小的std::arraystd::set,{ {1}}或自制容器。它应该探测std::vector,使back_insert_iterator失败,对insert_iterator( C.end() )等固定大小的容器进行特化(即使空的大小也是如此!)。这会使std::array变得更加复杂,而且它已经占据了这篇文章的大约一半!

最后,这是一个启用ADL的“容器支持开始”:

container_traits

请注意,namespace aux_adl { using std::begin; using std::end; template<typename C> auto adl_begin( C&& c )->decltype( begin(std::forward<C>(c)) ); template<typename C> auto adl_end( C&& c )->decltype( end(std::forward<C>(c)) ); template<typename C> auto adl_cbegin( C const& c )->decltype( begin(c) ); template<typename C> auto adl_cend( C const& c )->decltype( end(c) ); } template<typename C, typename=void> struct container_iterator {} template<typename C> struct container_iterator<C, std::enable_if< std::is_same< decltype( adl_aux::adl_begin(std::declval<C>()), decltype( adl_aux::adl_end(std::declval<C>()) >::value >::type> { typedef adl_aux::adl_begin(std::declval<C>()) iterator; }; template<typename C> struct container_const_iterator<C, std::enable_if< std::is_same< decltype( adl_aux::adl_cbegin(std::declval<C>()), decltype( adl_aux::adl_cend(std::declval<C>()) >::value >::type> { typedef adl_aux::adl_cbegin(std::declval<C>()) const_iterator; }; 类型函数只进行正确的ADL类型查找,实际上无法调用它们。

是的,这是一个黑客攻击。显然有人在讨论C ++委员会,允许adl_end内的using声明来解决这个问题。