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.h
和 tqdm/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使用模板提供了两种接口,即上面的tqdm
和 range
函数。这两个函数都是模板函数,可以接受任意类型的迭代器。我们先看看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
模板类是一个迭代器,模板参数是SizeType
,SizeType
默认是一个整数类型。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
类中有三个成员变量,分别是current
,total
,step
,这三个成员变量分别表示当前的迭代次数,总的迭代次数,每次迭代的步长。
RangeIterator
类中实现了operator++
运算符重载,operator*
运算符重载,operator!=
运算符重载,operator==
运算符重载,operator-
运算符重载,operator bool
运算符重载。这样就可以使用for
循环来遍历迭代器了。RangeIterator
类中还实现了size_remaining
函数,这个函数返回剩余的迭代次数。
在operator++
运算符中,每次迭代都会对current
进行自增操作,自增的步长是step
。这样就可以在range
函数中指定每次迭代的步长了。(这里要注意,step
不能为0,否则会出现死循环。)
至此,我们就分析完了tqdm.cpp
库的实现。这个库的代码量只有500多行,涉及到了模板编程,运算符重载,异常处理等知识点,非常适合cpp新手阅读。但是这个库只是一个半成品,只实现了进度条的显示,没有实现进度条的更新,也没有实现进度条的样式的修改。之后我们将来改造这个库,实现一个完整的进度条库。