这是一个很长的答案。简短的回答是“类型擦除”;去了解一下吧。
长答案是两个答案。首先,我介绍“您是否只想能够迭代连续的ints?”。然后你想要span。这是一种非常简单的类型擦除形式,只要它是连续的并且超过 T,它就会忘记您正在处理的确切容器是什么。
第二个答案是如果你真的需要处理多种类型(不仅仅是int)和多种容器(不仅仅是连续的)。
两个答案用一条线隔开。
span 概念(请参阅gsl::span)就是为了这个原因而设计的。它本身是一个模板(超过您正在使用的类型),但在大多数接口中它将是模板的具体实例。
这是它的玩具版:
template<class T>
struct span_t {
T* b = 0;
T* e = 0;
T* begin() const { return b; }
T* end() const { return e; }
span_t(span_t const&)=default;
span_t& operator=(span_t const&)=default;
span_t()=default;
span_t( T* s, T* f ):b(s),e(f) {}
span_t( T* s, std::size_t l):span_t(s, s+l){}
template<std::size_t N>
span_t( T(&arr)[N] ):span_t(arr, N) {}
std::size_t size() const { return end()-begin(); }
bool empty() const { return begin()==end(); }
T& front() const { return *begin(); }
T& back() const { return *(std::prev(end()); }
T* data() const { return begin(); }
span_t without_front( std::size_t N=1 ) const {
return {std::next( begin(), (std::min)(N, size()) ), end()};
}
span_t without_back( std::size_t N=1 ) const {
return {begin(), std::prev(end(), (std::min)(N, size()) )};
}
};
我们可以用转换运算符来扩充它
namespace details {
template<template<class...>class Z, class, class...Ts>
struct can_apply:std::false_type{};
template<class...>using void_t=void;
template<template<class...>class Z, class...Ts>
struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z,void,Ts...>;
template<class C>
using dot_data_r = decltype( std::declval<C>().data() );
template<class C>
using dot_size_r = decltype( std::declval<C>().size() );
template<class C>
using can_dot_data = can_apply< dot_data_r, C >;
template<class C>
using can_dot_size = can_apply< dot_size_r, C >;
can_dot_data 通过 SFINAE 检测 .data() 是否对 C 类型的对象有效。
现在我们添加一个构造函数:
template<class T,
std::enable_if_t<
can_dot_data<T&>{}
&& can_dot_size<T&>{}
&& !std::is_same<std::decay_t<T>, span_t>{}
, int
> =0
>
span_t( T&& t ): span_t( t.data(), t.size() ) {}
其中包括 std::vector 和 std::string 和 std::array。
你的函数现在看起来像:
void foo(span_t<int> s) {
for (auto&& e:s)
std::cout << s;
}
}
使用:
std::vector<int> a;
foo(a);
现在,这仅适用于特定类型的连续容器。
假设这不是你想要的。也许您确实需要为无数种类型解决这个问题,并且您不想在标头中公开所有内容。
那么你需要做的就是类型擦除。
您需要从提供的类型中找出您需要的最小操作集。然后,您需要编写将这些操作“类型擦除”到“无类型”操作的包装器。
这在标头中,或在另一个帮助标头中。
在函数的接口中,或者在标头中间帮助程序中,您获取传入的类型并进行类型擦除,然后将类型擦除的类型传递给“真正的”实现。
类型擦除的一个例子是std::function。它需要几乎任何可以使用固定签名调用的东西,并将其转换为单个类型擦除类型。除了如何复制、销毁和调用该类型的实例之外的所有内容都被“遗忘”或删除。
对于您的情况:
template <typename Iterator>
void foo(Iterator first, Iterator last) {
for (Iterator it = first; it!=last; ++it) {
cout << *it;
}
}
我看到有两件事需要删除;迭代和打印。
struct printable_view_t {
void const* data = 0;
void(*print_f)(std::ostream& os, void const*) = 0;
explicit operator bool()const{return data;}
printable_view_t() = default;
printable_view_t(printable_view_t const&) = default;
template<class T,
std::enable_if_t<!std::is_same<T, printable_view_t>{}, int> =0
>
printable_view_t( T const& t ):
data( std::addressof(t) ),
print_f([](std::ostream& os, void const* pv){
auto* pt = static_cast<T const*>(pv);
os << *pt;
})
{}
std::ostream& operator()(std::ostream& os)const {
print_f(os, data);
return os;
}
friend std::ostream& operator<<(std::ostream& os, printable_view_t p) {
return p(os);
}
};
printable_view_t 是类型擦除“我可以打印”的示例。
void bar( printable_view_t p ) {
std::cout << p;
}
void test_bar() {
bar(7);
bar(3.14);
bar(std::string("hello world"));
}
接下来我们要做的是键入擦除迭代。这更难,因为我们想要键入擦除迭代而不是迭代 printable_view_t 类型。
类型擦除 foreach 更容易一些,而且通常更有效。
template<class View>
struct foreach_view_t {
void* data = 0;
void(*func)( std::function<void(View)>, void* ) = 0;
explicit operator bool()const{return data;}
foreach_view_t() = default;
foreach_view_t(foreach_view_t const&) = default;
template<class T,
std::enable_if_t<!std::is_same<std::decay_t<T>, foreach_view_t>{}, int> =0
>
foreach_view_t( T&& t ):
data( const_cast<std::decay_t<T>*>(std::addressof(t)) ),
func([](std::function<void(View)> f, void* pv){
auto* pt = static_cast<std::remove_reference_t<T>*>(pv);
for (auto&& e : *pt)
f(decltype(e)(e));
})
{}
void operator()(std::function<void(View)> f)const{
func(f, data);
}
};
然后我们将这些菊花链在一起
void foo(foreach_view_t<printable_view_t> x) {
x([](auto p){ std::cout << p; });
}
测试代码:
std::vector<int> a{1,2,3};
foo(a);
现在大部分头代码被“提升”到类型擦除类型而不是函数模板主体中。但是仔细选择类型擦除点可以让你保持你需要的类型精确和狭窄,以及你如何使用这些操作的逻辑。
例如,上面的代码并不关心您将其打印到的位置; std::cout 不是类型擦除的一部分。
Live example.