读书频道 > 网站 > 网页设计 > 深入应用C++11:代码优化与工程级应用
1.4.3 让基于范围的 for循环支持自定义类型
15-07-07    下载编辑
收藏    我要投稿   
在StackOverflow的最近一次世界性调查中,C++11在所有的编程语言中排名第二, C++11受到程序员的追捧是毫不意外的,因为它就像C++之父Bjarne Stroustrup说的:它看起来就像一门新的语言。C++11新增加了相当多的立即去当当网订购

假如有一个自己自定义的容器类,如何让它能够支持range-based for呢?其实上面已经提到了,基于范围的for循环只是普通for循环的语法糖。它需要查找到容器提供的begin和end迭代器。

具体来说,基于范围的for循环将以下面的方式查找容器的begin和end:

1)若容器是一个普通array对象(如int arr[10]),那么begin将为array的首地址,end将为首地址加容器长度。

2)若容器是一个类对象,那么range-based for将试图通过查找类的begin()和end()方法来定位begin、end迭代器。

3)否则,range-based for将试图使用全局的begin和end函数来定位begin、end迭代器。

由上述可知,对自定义类类型来说,分别实现begin()、end()方法即可。下面通过自定义一个range对象来看看具体的实现方法。

我们知道,标准库中有很多容器,如vector、list、queue、map,等等。这些对象在概念上属于Containers。但是如果我们并不需要对容器进行迭代,而是对某个“区间”进行迭代,此时标准库中并没有对应的概念。

在这种情况下,只能使用普通的for循环,代码如下:

 

for (int n = 2; n < 8; n += 2) // [2, 8)
{
   std::cout << " " << n;
}


上面的for循环其实是在区间[2, 8)中做迭代,迭代步长为2。如果我们可以实现一个range方法,使用range(2, 8, 2)来代表区间[2, 8),步长为2,就可以通过基于范围的for循环直接对这个区间做迭代了。

我们来看一下range的实现。首先,需要一个迭代器来负责对一个范围取值。

考虑到for循环中迭代器的使用方法(结束条件:ite !=end),迭代器的对外接口可以像下面这样:

 

template <typename T>
class iterator
{
public:
using value_type = T;
using size_type  = size_t;

   iterator(size_type cur_start, value_type begin_val,
value_type step_val);

   value_type operator*() const;

   bool operator!=(const iterator& rhs) const;

   iterator& operator++(void); // pref?ix ++ operator only
};

构造函数传递3个参数来进行初始化,分别是开始的迭代次数、初始值,以及迭代的步长。如果容器的begin()方法将设置初始值cur_start为0,end()方法将限定最多能够迭代的次数,那么在对ite和end做比较的时候,只需要比较迭代的次数就可以了。由于迭代次数不可能是负数,所以类型使用size_t。

这里为何不直接使用iterator当中的值做比较呢?这是因为迭代的步长step并不一定是begin-end的约数。如果我们直接采用iterator中的值做比较,就不能使用!=了。

operator*()用于取得迭代器中的值。

operator!=用于和另一个迭代器做比较。因为在基于范围的for循环的迭代中,我们只需要用到!=,因此,只需要实现这一个比较方法。

operator++用于对迭代器做正向迭代。当step为负数时,实际上会减少iterator的值。同样,在基于范围的for循环的迭代中也只需要实现前置++。

整理出这些必需的接口之后,它的功能实现就不困难了,如代码清单1-11所示。

代码清单1-11 迭代器类的实现

 

namespace detail_range {

////////////////////////////////////////////////////////////////
/// The range iterator
////////////////////////////////////////////////////////////////

template <typename T>
class iterator
{
public:
   using value_type = T;
   using size_type = size_t;

private:
   size_type        cursor_;
   const value_type step_;
   value_type       value_;

public:
   iterator(size_type cur_start, value_type begin_val, value_type
step_val)
      : cursor_(cur_start)
      , step_  (step_val)
      , value_ (begin_val)
   {
      value_ += (step_ * cursor_);
   }

   value_type operator*() const { return value_; }

   bool operator!=(const iterator& rhs) const
   {
      return (cursor_ != rhs.cursor_);
   }

   iterator& operator++(void) // pref?ix ++ operator only
   {
      value_ += step_;
      ++ cursor_;
      return (*this);
   }
};

} // namespace detail_range

上述分别定义了cursor_、step_和value_这3个成员变量,来保存构造函数中传递进来的参数。

在iterator的构造中,我们通过一个简单的计算得到value_当前正确的值。

在之后的operator++(前置++)运算符中,我们通过+=操作计算出value_的下一个值。

在上面的代码中,step_被定义为一个常量。这是因为step_作为步长的存储,在迭代器中是不能被修改的。

接下来思考range类型的具体实现。

基于范围的for循环只需要对象具有begin和end方法,因此,impl类可以如代码清单1-12所示。

代码清单1-12 impl类的声明

 

template <typename T>
class impl
{
public:
   using value_type      = T;
   using reference       = const value_type&;
   using const_reference = const value_type&;
   using iterator        = const
detail_range::iterator<value_type>;
   using const_iterator  = const
detail_range::iterator<value_type>;
   using size_type       = typename iterator::size_type;

   impl(value_type begin_val, value_type end_val, value_type
step_val);
size_type size(void) const;

const_iterator begin(void)const;
const_iterator end(void)const;
};

impl是range函数最终返回的一个类似容器的对象,因此,我们给它定义了容器所拥有的基本的概念抽象,比如value_type、reference及iterator。

很明显,我们不希望外部能够修改range,这个range应当是一个仅能通过构造函数作一次性赋值的对象,因此reference及iterator都应该是const类型,并且begin、end接口也应当返回const_iterator。

下面,让我们来实现impl类,如代码清单1-13所示。

代码清单1-13 impl类的实现

 

namespace detail_range {

////////////////////////////////////////////////////////////////
/// The impl class
////////////////////////////////////////////////////////////////

template <typename T>
class impl
{
public:
   using value_type      = T;
   using reference       = const value_type&;
   using const_reference = const value_type&;
   using iterator        = const
detail_range::iterator<value_type>;
   using const_iterator  = const
detail_range::iterator<value_type>;
   using size_type       = typename iterator::size_type;

private:
   const value_type begin_;
   const value_type end_;
   const value_type step_;
   const size_type  max_count_;

   size_type get_adjusted_count(void) const
   {
      if (step_ > 0 && begin_ >= end_)
         throw std::logic_error("End value must be greater than begin value.");
      else
      if (step_ < 0 && begin_ <= end_)
         throw std::logic_error("End value must be less than begin value.");

      size_type x = static_cast<size_type>((end_ - begin_) /
step_);
      if (begin_ + (step_ * x) != end_) ++x;
      return x;
   }

public:
   impl(value_type begin_val, value_type end_val, value_type
step_val)
      : begin_(begin_val)
      , end_  (end_val)
      , step_ (step_val)
      , max_count_(get_adjusted_count())
   {}

   size_type size(void) const
   {
      return max_count_;
   }

   const_iterator begin(void) const
   {
      return { 0, begin_, step_ };
   }

   const_iterator end(void) const
   {
      return { max_count_, begin_, step_ };
   }
};

} // namespace detail_range

在上面的实现中,只允许impl做初始化时的赋值,而不允许中途修改,因此,它的成员变量都是const类型的。

通过private的get_adjusted_count方法,我们在获取max_count_也就是最大迭代次数时会先判断begin_、end_和step_的合法性。很显然,合法的组合将只可能计算出大于等于0的最大迭代次数。

之所以在get_adjusted_count中对计算的值做自增的调整,是为了让计算的结果向上取整。

最后,我们实现出range的外覆函数模板,如代码清单1-14所示。

代码清单1-14 range类的外覆函数模板

 

////////////////////////////////////////////////////////////////
/// Make a range of [begin, end) with a custom step
////////////////////////////////////////////////////////////////

template <typename T>
detail_range::impl<T> range(T end)
{
   return{ {}, end, 1 };
}
template <typename T>
detail_range::impl<T> range(T begin, T end)
{
   return{ begin, end, 1 };
}

template <typename T, typename U>
auto range(T begin, T end, U step)
   -> detail_range::impl<decltype(begin + step)>
{
   // may be narrowing
   using r_t = detail_range::impl<decltype(begin + step)>;
   return r_t(begin, end, step);
}

可以看到,通过初始化列表,可以让return的书写非常便捷。

这里需要注意的是第三个range重载。我们当然不希望限制用户使用range(1, 2, 0.1)这种方式来创建区间[1, 2),因此,step可以是不同的数据类型。

那么相对的,impl的模板参数类型应当是begin+step的返回类型。因此,我们需要通过返回值后置语法,并使用decltype(begin+step)来推断出range的返回值类型。

这样,在运行return的时候,可能会导致类型收窄(整型和浮点型的加法运算将总是返回浮点型),不过此处的类型收窄一般不会影响计算结果,因此,最后一个重载函数需要使用直接初始化方式返回impl对象。

下面运行一组示例来看看效果,如代码清单1-15所示。

代码清单1-15 测试代码

 

void test_range(void)
{
   std::cout << "range(15):";
   for (int i : range(15))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range(2, 6):";
   for (auto i : range(2, 6))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   const int x = 2, y = 6, z = 3;
   std::cout << "range(2, 6, 3):";
   for (auto i : range(x, y, z))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range(-2, -6, -3):";
   for (auto i : range(-2, -6, -3))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range(10.5, 15.5):";
   for (auto i : range(10.5, 15.5))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range(35, 27, -1):";
   for (int i : range(35, 27, -1))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range(2, 8, 0.5):";
   for (auto i : range(2, 8, 0.5))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range(8, 7, -0.1):";
   for (auto i : range(8, 7, -0.1))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;

   std::cout << "range('a', 'z'):";
   for (auto i : range('a', 'z'))
   {
      std::cout << " " << i;
   }
   std::cout << std::endl;
}

int main(void)
{
   test_range();
return 0;
}
示例的运行结果如下:
range(15): 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
range(2, 6): 2 3 4 5
range(2, 6, 3): 2 5
range(-2, -6, -3): -2 -5
range(10.5, 15.5): 10.5 11.5 12.5 13.5 14.5
range(35, 27, -1): 35 34 33 32 31 30 29 28
range(2, 8, 0.5): 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 7.5
range(8, 7, -0.1): 8 7.9 7.8 7.7 7.6 7.5 7.4 7.3 7.2 7.1
range('a', 'z'): a b c d e f g h i j k l m n o p q r s t u v w x y

通过上面的range实现,我们不仅完成了自定义类型的range-based for支持,同时也使用了我们前面所学的自动类型推导、模板别名以及初始化列表的相关知识。

我们完成的这个range,目前还有一些限制(比如参数传递采用值语义,不支持range返回值的存储后修改等)。如果读者感兴趣,可以自己进一步完善range的实现。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站