通用迭代器,无需使用模板c ++即可访问向量元素

时间:2017-06-01 07:02:47

标签: c++ c++11 templates vector iterator

我正在创建一个函数,该函数应该作为向量的输入迭代器 例如:

vector<int> a;
foo(a.begin(),a.end())

vector可以保留任何类型。

现在,执行此操作的简单方法是使用模板

template <typename Iterator>
void foo(Iterator first, Iterator last) {
    for (Iterator it = first; it!=last; ++it) {
        cout << *it;
    }
}

我想知道是否有办法在不使用模板的情况下实现相同的功能。由于使用模板会强制我将这些函数包含在我不想要的公共API的Header文件中。所以我想知道是否有另一种方法可以在不使用模板的情况下访问迭代器。

3 个答案:

答案 0 :(得分:3)

有些方法可以不在头文件中包含实现,但实现起来并不干净(例如,您应事先知道实例化)。请阅读此处了解有关此问题的更多信息:

例如:

foo.h中

#ifndef HI_
#define HI_

template<class Iterator>
void foo(Iterator first, Iterator last);

#endif

Foo.cpp中

#include "stack.h"
using namespace std;


template<class Iterator>
void foo(Iterator first, Iterator last) {
    for (Iterator it = first; it != last; ++it) {
        cout << *it << " ";
    }
}

template
void foo( std::vector<int>::iterator  first,   std::vector<int>::iterator  last);

template
void foo( std::vector<double>::iterator  first,   std::vector<double>::iterator  last);

现在,您只能对foodouble使用int功能。其他类型不会链接。

希望这有帮助。

答案 1 :(得分:1)

  

我想知道是否有办法在不使用模板的情况下实现相同的功能。 [...]我想知道是否有另一种方法可以在不使用模板的情况下访问迭代器。

是的,如果您使用的是C ++ 14,但是......

  

由于使用模板会强制我将这些功能包含在我不想要的公共API的Header文件中。

...对您来说不是一种有用的方式,因为它等同于使用模板,您必须将它放在头文件中。

在C ++ 14中,您可以使用带有auto参数的lambda函数。

auto foo = [](auto first, auto last)
 { for (auto it = first ; it != last; ++it ) std::cout << *it; };

auto不是模板(从形式上看)但是相同,你不能在标题中声明foo并在cpp文件中开发它。

答案 2 :(得分:1)

这是一个很长的答案。简短的回答是“类型擦除”;去了解它。

答案很长,有两个答案。首先,我介绍“您是否希望能够迭代连续的int?”。那你想要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::vectorstd::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