两者都教我自己实现更多高级模板结构而不仅仅是基本模板结构,并且因为它们在许多情况下都很有用,我正在尝试使用c ++ 11结构实现函数式编程中常见的map,filter和类似函数decltype。
我在创建我使用的编译器可以处理的函数原型时遇到问题,所以我不得不问你如何创建这样的东西:
//
// Takes an iterable, applies a function to every element, and returns a vector of the results
//
template <typename T, typename Func>
auto map(const T& iterable, Func func) -> std::vector< decltype( func( *iterable.cbegin() ) ) >
{
// body snipped
}
也就是说,这个函数应该采用任何iterable和一个函数,它将iterables值类型作为参数并返回某种值。函数调用的结果将是传递函数返回的类型的向量,无论传入的可迭代类型如何。
map函数应该接受任何带有有效原型作为参数的函数,无论它是函数指针,函子还是lambda表达式。
将以上函数与此测试代码一起使用:
std::vector<int> intVector;
intVector.push_back(1);
intVector.push_back(2);
map(intVector, [](int& value) { return value + 1; });
让visual studio吐出C2893(“专业化功能模板失败”)错误,我不确定是什么问题。
更新 到目前为止,在评论和答案中建议应用的更改,新的原型测试,但仍然存在相同的错误。
答案 0 :(得分:6)
这可能会做你想要的。它在内部使用std::transform
,基本上完成了整个工作。我写的函数只不过是一个简单的容器包装器(不使用C风格的数组,需要一些额外的类型特性):
#include <vector>
#include <algorithm>
#include <type_traits>
//
// Takes an iterable, applies a function to every element,
// and returns a vector of the results
//
template <typename T, typename Func>
auto map_container(const T& iterable, Func&& func) ->
std::vector<decltype(func(std::declval<typename T::value_type>()))>
{
// Some convenience type definitions
typedef decltype(func(std::declval<typename T::value_type>())) value_type;
typedef std::vector<value_type> result_type;
// Prepares an output vector of the appropriate size
result_type res(iterable.size());
// Let std::transform apply `func` to all elements
// (use perfect forwarding for the function object)
std::transform(
begin(iterable), end(iterable), res.begin(),
std::forward<Func>(func)
);
return res;
}
但是,请注意,您的lambda应该引用const
,或者更好的是应该在int
的情况下按值获取其参数。
此外,我将函数从map
重命名为map_container
:对于函数,变量或程序中的任何其他内容,重用C ++标准库的标准容器名称是一种错误的编程习惯。
对我来说,这给出了所需的输出:
#include <iostream>
int main()
{
std::vector<int> intVector;
intVector.push_back(1);
intVector.push_back(2);
auto v = map_container(intVector, [] (int value) { return value + 1; });
for (int i : v) { std::cout << i << " "; }
}
答案 1 :(得分:4)
所以这里有一大堆角落案件需要处理。我要做的是首先尝试构建一些container_traits
模板,以尽可能多地抽象工作。
如果类型允许调用container
和begin
免费函数,end
和std::begin
已被发挥作用,则该类型为std::end
通过using
,这两种类型是相同的(最后可能不是必需的)。
container
的特征主要来自容器所具有的iterator
,以及所述迭代器的类型。其他一些功能,例如size
(甚至size_at_least
- 见下文),很常见。
如果该类型的iterable
为const
,则称该类型为container
。
接下来的问题是“哪种类型的实例对于映射容器的元素有效?” - 这也有点不重要,所以我添加了一些特征类来处理它。
因此,这导致了这种实现:
#include <algorithm>
#include <type_traits>
#include <utility>
namespace aux {
// calculate the type that calling `begin` and `end` on a type will return
// in a scope where `std::begin` and `std::end` are visible. This hack is
// required to enable argument-dependent lookup.
using std::begin;
using std::end;
template<typename T>
auto adl_begin(T&&t)->decltype( begin(std::forward<T>(t)) );
template<typename T>
auto adl_end(T&&t)->decltype( end(std::forward<T>(t)) );
template<typename T>
auto adl_cbegin(T const&t)->decltype( begin(t) );
template<typename T>
auto adl_cend(T const&t)->decltype( end(t) );
}
// What is a container? Something with a `begin`ing and an `end`ing...
template<typename C,typename=void>
struct is_container:std::false_type {};
template<typename C>
struct is_container<C, typename std::enable_if<
std::is_same<
decltype(aux::adl_begin(std::declval<C>())),
decltype(aux::adl_end(std::declval<C>()))
>::value
>::type >:std::true_type {};
// Default container_traits is empty for SFINAE ease of use:
template<typename C, typename=void>
struct container_traits {};
// if it is a container, go in whole hog:
template<typename C>
struct container_traits<C, typename std::enable_if< is_container<C>::value >::type >
{
typedef decltype( aux::adl_begin(std::declval<C>()) ) iterator;
typedef decltype( aux::adl_cbegin(std::declval<C>()) ) const_iterator;
// I'm lazy, so I'll copy typedefs from `iterator_traits` below:
typedef typename std::iterator_traits<iterator>::value_type value_type;
typedef typename std::iterator_traits<iterator>::reference reference;
// etc
// TODO: size_at_least is a helper function
// it returns 0 if it is expensive to calculate the size (say, a range
// if iterators into a `std::list`), and the size if it is cheap to
// calculate (say, a `std::vector`, any class with a `.size()` method,
// or a pair of pointers or other random-access iterators)
// template<typename C2, typename=typename std::enable_if< std::is_convertable< C2, C const&>::value>::type
// static std::size_t size_at_least( C2&& c ) { ... }
};
// Can Functor map the elements of C into something we can store elsewhere?
template<typename C, typename Functor, typename=void>
struct can_map:std::false_type {};
// Yes, if the result of calling Functor on C's elements is non-void:
template<typename C, typename Functor>
struct can_map<C, Functor, typename std::enable_if<
!std::is_same< decltype(std::declval<Functor>()(std::declval<typename container_traits<C>::value_type>())), void >::value
>::type>: std::true_type {};
// The result of mapping the elements of C under Functor
template<typename C, typename Functor, typename=void>
struct map_result {};
template<typename C, typename Functor>
struct map_result<C,Functor,typename std::enable_if< can_map<C,Functor>::value>::type>
{
typedef
decltype(
std::declval<Functor>()(
*std::declval<
typename container_traits<C>::const_iterator
>()
)
)
type;
};
// The actual implementation
// we std::decay the map_result because we want to store
// instances of the type, and std::decay does that quite nicely
// note that some pathological Functors may break this, ie ones
// that return pseudo-references that are intended to be read from
// yet are not std-container safe
template <typename T, typename Func>
auto map_container(T&& iterable, Func&& func) ->
std::vector<
typename std::decay<
typename map_result<T, Func>::type
>::type
>
{
std::vector<
typename std::decay<
typename map_result<T, Func>::type
>::type
> retval;
// TODO: use container_traits<T>::size_at_least to reserve space in retval
// that will bring the efficiency of this function up to near-hand-crafted-C.
for (auto&& s:iterable) {
retval.push_back( func(s) );
}
return retval;
}
就是这样。接下来,测试代码。我们应该能够{C}类数组map_container
,传统类型vector
和bool
(使用伪引用并紧密包装),以及用户定义通过.begin()
方法和自由浮动begin(C)
函数进行输入。
我对数组的一个问题是C const&
似乎导致数组中的指针衰减,这使得它不再是一个容器:我必须绑定到C&&
才能获得真正的数组类型。
#include <iostream>
void test1() {
std::vector<int> src{1,2,3,4,5};
auto r = map_container( src, [](int x){return x*2;});
for (auto&& x:r) {
std::cout << x << "\n";
}
}
struct test_buffer {
int foo[5];
int* begin() { return foo; }
int* end() { return &foo[5]; }
int const* begin() const { return foo; }
int const* end() const { return &foo[5]; }
};
test_buffer buff1={{1,2,3,4,5}};
struct test_buffer_2 {
int foo[5];
};
test_buffer_2 buff2={{1,2,3,4,5}};
int* begin(test_buffer_2& t) { return t.foo; }
int* end(test_buffer_2& t) { return &t.foo[5]; }
int const* begin(test_buffer_2 const& t) { return t.foo; }
int const* end(test_buffer_2 const& t) { return &t.foo[5]; }
std::vector<bool> bits{true, false, true, false};
template<typename Container>
void tester(Container&& c) {
Container const& src = c;
auto r = map_container( src, [](int x){return x*2;});
for (auto&& x:r) {
std::cout << x << "\n";
}
}
void test2() {
tester(buff1);
tester(buff2);
tester(bits);
}
template<typename C>
bool is_container_test(C&&) {
return is_container<C>::value;
}
template<typename C, typename F>
bool can_map_test( C&&, F&& ) {
return can_map<C, F>::value;
}
template<typename C, typename F>
bool can_map_test2( C const&, F&& ) {
return can_map<C, F>::value;
}
int array[] = {1,2,3,4,5};
void test3() {
std::cout << "Array is container:" << is_container_test(array) << "\n";
auto x2 = [](int x){return x*2;};
std::cout << "Double can map:" << can_map_test(array, x2) << "\n";
std::cout << "Double can map:" << can_map_test2(array, x2) << "\n";
}
void test4() {
tester(array);
}
int main() {
test1();
test2();
test3();
test4();
}
或类似的东西。不要在函数本身中执行复杂的SFINAE,而是创建为您工作的特征类。
上面使用的其他技术:我使用std::begin
和std::end
来获取开始/结束迭代器。这意味着我现在支持原始C数组。然后我将其包含在一些依赖于参数的查找助手中,其目的是允许您在同一名称空间中使用类覆盖来定义begin
和end
。
请注意container_traits
的“不接受”版本是一个空结构,而不是未定义的结构。这使我们可以在其他地方的SFINAE中使用container_traits
。
哦,提高效率就是编写“智能储备”,它采用带有reserve
方法的容器和要复制其大小的容器。如果要复制的容器缺少随机访问迭代器并且缺少.size()
方法,则它不会执行任何操作,但如果这样做,则会执行.reserve( end(...)-begin(...) )
或.reserve(src.size())
。我们可以将其添加到container_traits
作为static size_t size_at_least(Container const&)
,以便在O(1)时间内返回size_t
,而不是大于{{1}的大小。 }。
答案 2 :(得分:0)
Andy Prowl's excellent answer above的一些小改进:
您可以使用此in Compiler Explorer,但代码本身如下所示:
template <typename Iterator, typename Func> [[nodiscard]]
auto functional_map(Iterator begin, Iterator end, Func && func) ->
std::vector<decltype(func(std::declval<typename Iterator::value_type>()))>
{
using value_type = decltype(func(std::declval<typename Iterator::value_type>()));
std::vector<value_type> out_vector;
out_vector.reserve(std::distance(begin, end));
std::transform(begin, end, std::back_inserter(out_vector),
std::forward<Func>(func));
return out_vector;
}
template <typename T, typename Func> [[nodiscard]]
auto functional_map(const T & iterable, Func && func) ->
std::vector<decltype(func(std::declval<typename T::value_type>()))>
{
return functional_map(std::begin(iterable), std::end(iterable),
std::forward<Func>(func));
}
(请注意,我在这些声明中使用的是C ++ 17的[[nodiscard]]
属性,但如果您是C ++ 17之前的版本,则可以毫无问题地删除它。)
Compiler Explorer链接还包括一些示范性测试:
TEST_CASE("Mapping ints to string")
{
const std::vector<int> int_version{0, 1, 2, 3, 4};
const std::vector<std::string> string_version{"0", "1", "2", "3", "4"};
CHECK(functional_map(int_version,
[](int i) { return std::to_string(i); }) == string_version);
CHECK(functional_map(string_version,
[](const std::string & s) { return std::stoi(s); }) == int_version);
}
TEST_CASE("Mapping over only part of a container")
{
const std::vector<int> int_version{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
const std::vector<std::string> string_version{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
const std::vector<std::string> first_four_strings(string_version.begin(), string_version.begin() + 4);
CHECK(functional_map(int_version.begin(), int_version.begin() + 4,
[](int i) { return std::to_string(i); }) == first_four_strings);
const std::vector<int> first_four_ints(int_version.begin(), int_version.begin() + 4);
CHECK(functional_map(string_version.begin(), string_version.begin() + 4,
[](const std::string & s) { return std::stoi(s); }) == first_four_ints);
}