【问题标题】:generic iterators to access elements of vectors without using Templates c++通用迭代器在不使用模板 c++ 的情况下访问向量的元素
【发布时间】:2017-06-01 07:02:47
【问题描述】:

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

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 的头文件中。所以我想知道是否有另一种方法可以在不使用模板的情况下访问迭代器。

【问题讨论】:

  • 您需要绝对支持所有类型吗?
  • 没有。 (此空间特意留空)
  • 大部分应该支持向量
  • 那么请收下一对std::vector&lt;double&gt;::iterators。你不能一边吃一边吃蛋糕:要么自己选择元素类型,要么使用模板来支持它们。
  • 解决这个问题的一种肮脏方法是使用 void* 指针并传递单个元素的大小。这仍然会导致问题,因为当您想取消引用指针时您不知道类型。这意味着您必须传递另一个变量来说明您要将其转换为哪种类型的指针.... 这意味着您不能支持每种数据类型

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


【解决方案1】:

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

例如:

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);

现在您只能将foo 函数用于doubleint。其他类型不会链接。

希望这会有所帮助。

【讨论】:

    【解决方案2】:

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

    长答案是两个答案。首先,我介绍“您是否只想能够迭代连续的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::vectorstd::stringstd::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.

    【讨论】:

      【解决方案3】:

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

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

      因为使用模板会迫使我将这些函数包含在我不想要的公共 API 的头文件中。

      ...对你来说不是一个有用的方法,因为它相当于使用模板,你必须把它放在头文件中。

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

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

      autos 不是模板(从正式的角度来看),但它们是等价的,您不能在标头中声明 foo 并在 cpp 文件中开发它。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-06-28
        • 2020-03-27
        • 2020-02-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-07-26
        相关资源
        最近更新 更多