我有这个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
但这感觉不对。
你能想出更好的解决方案吗?
答案 0 :(得分:2)
我使用&#34;懒惰适配器&#34;解决了这个问题。正如Xeo所说。但是我没有使用Boost,而是写了一个非常简单的:
template<class R, class Fn>
class MapAdaptor
{
const R ⦥
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::array
,std::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
声明来解决这个问题。