C++新手进阶项目之tqdm.cpp源码剖析

tqdm.cpp

今天给大家介绍一个适合cpp新手进阶的库,叫做tqdm.cpp。代码量只有500多行,很适合cpp新手阅读。虽然是一个半成品的库,但是可以学习到很多cpp的编程技巧。今天我们就来分析一下这个库的实现,然后对这个库进行改造,实现一个完整的进度条库。

什么是tqdm.cpp

tqdm是一个python中非常著名的进度条库,可以包装迭代器来显示迭代器的进度。但是
tqdm只能用于python,不能用于c++。tqdm的作者也开发了tqdm的c++版本,可以用于c++的迭代器的包装,可以显示迭代器的进度,叫做tqdm.cpp。
开源地址:https://github.com/tqdm/tqdm.cpp

使用方法

tqdm.cpp是一个header-only的库,只需要包含头文件 tqdm/tqdm.htqdm/utils.h 即可。

使用方法:只是需要包含头文件 tqdm/tqdm.h,然后使用 tqdm::tqdm 函数包装迭代器即可。除了迭代器,也可以包装容器,只要容器提供了 begin()end() 函数即可。同时,也可以使用 range 函数生成一个迭代器,用于包装。

#include "tqdm/tqdm.h"

int main() 
{
    for (int i:tqdm::range(1000))
    {
        // do something
    }

    // or
    std::vector<int> v(1000);
    for(int &i : tqdm::tqdm(v.begin(), v.end())) 
    {
        // do something
    }
}

代码分析

tqdm.cpp使用模板提供了两种接口,即上面的tqdmrange 函数。这两个函数都是模板函数,可以接受任意类型的迭代器。我们先看看tqdm函数的实现。

// 两个迭代器的版本
template <typename _Iterator, typename _Tqdm = Tqdm<_Iterator>>
_Tqdm tqdm(_Iterator begin, _Iterator end) {
  return _Tqdm(begin, end);
}
// 一个迭代器和一个size_t的版本
template <typename _Iterator, typename _Tqdm = Tqdm<_Iterator>>
_Tqdm tqdm(_Iterator begin, size_t total) {
  return _Tqdm(begin, total);
}
// 一个容器的版本
template <typename _Container,
          typename _Tqdm = Tqdm<typename _Container::iterator>>
_Tqdm tqdm(_Container &v) {
  return _Tqdm(v);
}

tqdm函数有三个重载版本,分别是接受迭代器的两个版本和接受容器的一个版本。这三个版本都是模板函数,可以接受任意类型的迭代器或者容器。这三个函数的返回值都是一个Tqdm模板类的对象。
这里用到了cpp中的函数模板的默认模板参数,如果用户没有指定模板参数,就使用默认的模板参数。这里的默认模板参数是Tqdm<_Iterator>,这个模板参数是一个模板类,模板参数是迭代器的类型。这里的Tqdm模板类是一个模板类,模板参数是迭代器的类型。

在前面的使用方法中,示例使用的是范围for循环,范围for循环实际上是cpp11中的语法糖,可以被编译器转换为普通的for循环。举个例子,下面的代码使用了范围for循环,实际上编译器会将其转换为普通的for循环。

std::vector<int> v{1,2,3,4,5};
for(auto & i :v)
{
    // do something
}

// 编译器会将上面的代码转换为
std::vector<int, std::allocator<int> > & __range1 = v;
__gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int>>> __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int>>> __end1 = __range1.end();
for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
    int & i = __begin1.operator*();
}

我们大致可以猜测,Tqdm模板类应该实现了operator++operator*operator!=
运算符以及begin()end()等函数 ,这样就可以使用for循环来遍历迭代器了。

接下来我们看一下Tqdm类的定义。

struct Params {
  std::string desc;  
  size_t total = -1;
  bool leave = true;
  FILE *f = stderr;
  int ncols = -1;
  float mininterval = 0.1f, maxinterval = 10.0f;
  unsigned miniters = -1;
  std::string ascii = " 123456789#";
  bool disable = false;
  std::string unit = "it";
  bool unit_scale = false;
  bool dynamic_ncols = true;
  float smoothing = 0.3f;
  std::string bar_format;
  size_t initial = 0;
  int position = -1;
  bool gui = false;
};

template <typename _Iterator>
class Tqdm : public MyIteratorWrapper<_Iterator> {
private:
  using TQDM_IT = MyIteratorWrapper<_Iterator>;
  _Iterator e;  
  Params self; 

public:

  Tqdm &begin() { return *this; }
  const Tqdm &begin() const { return *this; }
  Tqdm end() const { return Tqdm(e, e); }

  explicit operator _Iterator() { return this->get(); }

  // 对应第一个重载版本
  explicit Tqdm(_Iterator begin, _Iterator end)
      : TQDM_IT(begin), e(end), self() {
    self.total = size_t(end - begin);
  }

    //对应第二个重载版本
  explicit Tqdm(_Iterator begin, size_t total)
      : TQDM_IT(begin), e(begin + total), self() {
    self.total = total;
  }

    //对应第三个重载版本
  template <typename _Container,
            typename = typename std::enable_if<
                !std::is_same<_Container, Tqdm>::value>::type>
  Tqdm(_Container &v) : TQDM_IT(std::begin(v)), e(std::end(v)), self() {
    self.total = e - this->get();
  }

  explicit operator bool() const { return this->get() != e; }

  virtual void _incr() const override {
    //... 显示进度条的代码
  }
  virtual void _incr() override { ((Tqdm const &)*this)._incr(); }
};

可以发现Tqdm中有三个构造函数,分别对应三个重载版本,分别对应三个tqdm函数的三个重载版本。但是Tqdm类中并没有包含operator++运算符重载,那么tqdm函数返回的对象是如何实现operator++运算符重载的呢?
观察Tqdm类的定义,我们可以发现Tqdm类继承了一个模板类MyIteratorWrapper,如果Tqdm没有实现,那么这个父类中就应该实现了operator++运算符重载,不然就无法通过编译。我们来看一下MyIteratorWrapper类的定义。

template <typename _Iterator>
class MyIteratorWrapper
    : public std::iterator<
          std::forward_iterator_tag,
          typename std::iterator_traits<_Iterator>::value_type> {
  mutable _Iterator p;  

public:
  typedef typename std::iterator_traits<_Iterator>::value_type value_type;

  explicit MyIteratorWrapper(_Iterator x) : p(x) {}
  // default construct gives end
  MyIteratorWrapper() : p(nullptr) {}
  explicit MyIteratorWrapper(const MyIteratorWrapper &mit) : p(mit.p) {}

  // override this in Tqdm class
  virtual void _incr() { ++p; }
  // override this in Tqdm class
  virtual void _incr() const { ++p; }

  MyIteratorWrapper &operator++() {
    _incr();
    return *this;
  }
  const MyIteratorWrapper &operator++() const {
    _incr();
    return *this;
  }
  MyIteratorWrapper operator++(int) const {
    MyIteratorWrapper tmp(*this);
    _incr();
    return tmp;
  }
  template <class Other>
  bool operator==(const MyIteratorWrapper<Other> &rhs) const {
    return p == rhs.p;
  }
  template <class Other>
  bool operator!=(const MyIteratorWrapper<Other> &rhs) const {
    return p != rhs.p;
  }
  template <class Other>
  size_t operator-(const MyIteratorWrapper<Other> &rhs) {
    return p - rhs.p;
  }

  value_type &operator*() {
    return *p;
  }
  const value_type &operator*() const {
    return *p;
  }

  value_type &operator->() {
    return *p;
  }
  const value_type &operator->() const {
    return *p;
  }

  _Iterator &get() { return p; }
  const _Iterator &get() const { return p; }

  void swap(MyIteratorWrapper &other) noexcept { std::swap(p, other.p); }

  template <typename = typename std::is_pointer<_Iterator>>
  explicit operator bool() const {
    return p != nullptr;
  }
};

MyIteratorWrapper是一个模板类,模板参数是迭代器的类型。MyIteratorWrapper继承了std::iterator类,用来标记迭代器的类型。MyIteratorWrapper类中有一个成员变量p,这个成员变量是迭代器的类型。

回到前面的问题,MyIteratorWrapper类中果然实现了operator++运算符重载,Tqdm类继承了MyIteratorWrapper类,所以Tqdm类中也有operator++运算符重载。这样就可以使用for循环来遍历迭代器了。同时还实现了operator*operator->等运算符重载,这样就可以使用*->来访问迭代器了。
现在迭代器的遍历问题已经解决了,但是tqdm函数返回的对象是如何实现进度条的呢?

我们发现,MyIteratorWrapper类中的operator++运算符中调用了_incr函数,但是_incr函数只是简单的对迭代器进行了自增操作,这个函数是一个虚函数,需要在Tqdm类中进行重载。我们来看一下Tqdm类中的_incr函数的实现。

virtual void _incr() const override {
    if (this->get() == e)
        throw std::out_of_range("exhausted");  

    TQDM_IT::_incr();
    if (this->get() == e) {
        printf("\nfinished: %" PRIu64 "/%" PRIu64 "\n",
        static_cast<std::uint64_t>(self.total),
        static_cast<std::uint64_t>(self.total));
    } else
        printf("\r%" PRIi64 " left", (int64_t)(e - this->get()));
}

_incr函数中首先判断迭代器是否到达了末尾,如果到达了末尾,就抛出一个异常。如果没有到达末尾,就调用TQDM_IT::_incr()函数,这个函数是MyIteratorWrapper类中的_incr函数,这个函数只是简单的对迭代器进行了自增操作。然后判断迭代器是否到达了末尾,如果到达了末尾,就打印finished,否则打印剩余的迭代次数。
这里要补充一个知识点,就是\r,这个字符的作用是将光标移动到行首,然后再打印剩余的迭代次数,这样就可以实现进度条的效果了。
至此,我们就分析完了tqdm函数的实现。有同学可能发现了,_incr函数仅打印了剩余的迭代次数,并没有打印进度条,那么进度条是如何实现的呢?这个问题我们放在改造这个库的时候再解决。

接下来我们来分析一下range函数的实现。

template <typename SizeType = int>
using RangeTqdm = Tqdm<RangeIterator<SizeType>>;
template <typename SizeType> RangeTqdm<SizeType> range(SizeType n) {
  return RangeTqdm<SizeType>(RangeIterator<SizeType>(n),
                             RangeIterator<SizeType>(n));
}
template <typename SizeType>
RangeTqdm<SizeType> range(SizeType start, SizeType end) {
  return RangeTqdm<SizeType>(RangeIterator<SizeType>(start, end),
                             RangeIterator<SizeType>(start, end));
}
template <typename SizeType>
RangeTqdm<SizeType> range(SizeType start, SizeType end, SizeType step) {
  return RangeTqdm<SizeType>(RangeIterator<SizeType>(start, end, step),
                             RangeIterator<SizeType>(start, end, step));
}

tqdm函数类似,range函数也有三个重载版本。range函数的返回值是一个RangeTqdm模板类的对象。RangeTqdm是Tqdm模板类的一个别名,模板参数是RangeIterator模板类,RangeIterator模板类是一个迭代器,模板参数是SizeTypeSizeType默认是一个整数类型。RangeIterator模板类的定义如下。

template <typename IntType = int>
class RangeIterator
    : public std::iterator<std::forward_iterator_tag, IntType> {
private:
  mutable IntType current;
  IntType total;
  IntType step;

public:
  RangeIterator(IntType total) : current(0), total(total), step(1) {}
  RangeIterator(IntType start, IntType total)
      : current(start), total(total), step(1) {}
  RangeIterator(IntType start, IntType total, IntType step)
      : current(start), total(total), step(step) {}
  IntType &operator*() { return current; }
  const IntType &operator*() const { return current; }
  RangeIterator &operator++() {
    current += step;
    return *this;
  }
  const RangeIterator &operator++() const {
    current += step;
    return *this;
  }
  RangeIterator operator++(int) const {
    RangeIterator tmp(*this);
    operator++();
    return tmp;
  }
  explicit operator bool() const { return current < total; }
  size_t size_remaining() const { return (total - current) / step; }

  bool operator!=(const RangeIterator &) const { return current < total; }
  bool operator==(const RangeIterator &) const { return current >= total; }
  IntType operator-(const RangeIterator &it) const {
    return it.size_remaining();
  }
};

RangeIterator类中有三个成员变量,分别是currenttotalstep,这三个成员变量分别表示当前的迭代次数,总的迭代次数,每次迭代的步长。
RangeIterator类中实现了operator++运算符重载,operator*运算符重载,operator!=运算符重载,operator==运算符重载,operator-运算符重载,operator bool运算符重载。这样就可以使用for循环来遍历迭代器了。RangeIterator类中还实现了size_remaining函数,这个函数返回剩余的迭代次数。
operator++运算符中,每次迭代都会对current进行自增操作,自增的步长是step。这样就可以在range函数中指定每次迭代的步长了。(这里要注意,step不能为0,否则会出现死循环。)

至此,我们就分析完了tqdm.cpp库的实现。这个库的代码量只有500多行,涉及到了模板编程,运算符重载,异常处理等知识点,非常适合cpp新手阅读。但是这个库只是一个半成品,只实现了进度条的显示,没有实现进度条的更新,也没有实现进度条的样式的修改。之后我们将来改造这个库,实现一个完整的进度条库。

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值