最近在看候捷的C++系列视频,其中有一个例子讲到了使用迭代器特性(iterator_traits)和使用模版模版参数(template template parameter)以及模版别名(alias template)来完成同一件事情,非常的经典,这里记录一下。
事情的起因
事情的起因是要比较在不同类型的容器类型(vector、deque、set等等)中,对象是否有moveable的相关构造函数对容器各种操作性能的影响。所以要编写一个通用的模版方法,允许接受不同的模版参数,以及不同的数据类型,并能输出调用各种操作(拷贝构造,移动构造,赋值构造)耗费的时间。
为了方便进行,这里编写了两个简化版本的String类,一个名为MyString的类带有各种移动构造,拷贝,赋值函数(Big Five)
#include <iostream>
#include <cstring>
#include <string>
class MyString
{
public:
static size_t DCtor; // 累计 默认构造函数 调用次数
static size_t Ctor; // 累计 构造函数 调用次数
static size_t CCtor; // 累计 拷贝构造函数 调用次数
static size_t CAssign; // 累计 拷贝赋值构造函数 调用次数
static size_t MCtor; // 累计 移动构造函数 调用次数
static size_t MAssign; // 累计 移动赋值构造函数 调用次数
static size_t Dtor; // 累计 析构函数 调用次数
private:
char *_data;
size_t _length;
void _init_data(const char *s)
{
_data = new char[_length + 1];
memcpy(_data, s, _length);
_data[_length] = '\0';
}
public:
// default ctor
MyString() : _data(nullptr), _length(0)
{
++DCtor;
}
// ctor
MyString(const char *p) : _length(strlen(p))
{
++Ctor;
_init_data(p);
}
// copy ctor
MyString(const MyString &s) : _length(s._length)
{
++CCtor;
_init_data(s._data);
}
// move ctor with noexcept
MyString(MyString &&str) noexcept : _data(str._data), _length(str._length)
{
++MCtor;
str._length = 0;
str._data = nullptr;
}
// copy assignment
MyString &operator=(const MyString &str)
{
++CAssign;
if (this != &str)
{
if (_data)
delete _data;
_length = str._length;
_init_data(str._data);
}
return *this;
}
// move assignment
MyString &operator=(MyString &&str) noexcept
{
++MAssign;
if (this != &str)
{
if (_data)
delete _data;
_length = str._length;
_data = str._data;
str._length = 0;
str._data = nullptr;
}
return *this;
}
virtual ~MyString()
{
++Dtor;
if (_data)
delete _data;
}
bool operator<(const MyString &rhs) const
{
return std::string(this->_data) < std::string(rhs._data);
}
bool operator==(const MyString &rhs) const
{
return std::string(this->_data) == std::string(rhs._data);
}
char *get() const
{
return _data;
}
};
size_t MyString::DCtor = 0;
size_t MyString::Ctor = 0;
size_t MyString::CCtor = 0;
size_t MyString::CAssign = 0;
size_t MyString::MCtor = 0;
size_t MyString::MAssign = 0;
size_t MyString::Dtor = 0;
namespace std
{
template <>
struct hash<MyString>
{
size_t operator()(const MyString &s) const noexcept
{
return hash<string>()(string(s.get()));
}
};
}
另一个名为MyStringNoMove的类不带各种移动构造(Big Three)。
#include <cstring>
#include <iostream>
#include <string>
class MyStringNoMove
{
public:
static size_t DCtor; // 累计 默认构造函数 调用次数
static size_t Ctor; // 累计 构造函数 调用次数
static size_t CCtor; // 累计 拷贝构造函数 调用次数
static size_t CAssign; // 累计 拷贝赋值构造函数 调用次数
static size_t MCtor; // 累计 移动构造函数 调用次数
static size_t MAssign; // 累计 移动赋值构造函数 调用次数
static size_t Dtor; // 累计 析构函数 调用次数
private:
char *_data;
size_t _length;
void _init_data(const char *s)
{
_data = new char[_length + 1];
memcpy(_data, s, _length);
_data[_length] = '\0';
}
public:
// default ctor
MyStringNoMove() : _data(nullptr), _length(0)
{
++DCtor;
}
// ctor
MyStringNoMove(const char *p) : _length(strlen(p))
{
++Ctor;
_init_data(p);
}
MyStringNoMove(const MyStringNoMove &s) : _length(s._length)
{
++CCtor;
_init_data(s._data);
}
MyStringNoMove &operator=(const MyStringNoMove &s)
{
++CAssign;
if (this != &s)
{
if (_data != nullptr)
delete _data;
_length = s._length;
_init_data(s._data);
}
return *this;
}
virtual ~MyStringNoMove()
{
++Dtor;
if (_data)
delete _data;
}
char *get() const { return _data; }
bool operator<(const MyStringNoMove &rhs) const
{
return std::string(_data) < std::string(rhs._data);
}
bool operator==(const MyStringNoMove &rhs) const
{
return std::string(_data) == std::string(rhs._data);
}
};
size_t MyStringNoMove::DCtor = 0;
size_t MyStringNoMove::Ctor = 0;
size_t MyStringNoMove::CCtor = 0;
size_t MyStringNoMove::CAssign = 0;
size_t MyStringNoMove::MCtor = 0;
size_t MyStringNoMove::MAssign = 0;
size_t MyStringNoMove::Dtor = 0;
namespace std
{
template <>
struct hash<MyStringNoMove>
{
size_t operator()(const MyStringNoMove &s) const noexcept
{
return hash<string>()(string(s.get()));
}
};
}
这两个类,除了定义各种赋值、拷贝、移动方法之外,还定义了一些static变量用来统计各种构造的调用次数,以及重载了==和<操作符以方便能够放入到关联容器中。
有了这两个类,对于不同种容器,当然可以针对每种容器编写一个测试用例,但这样就会有很多重复代码,不够优雅,所以解决方法就是使用模版编程,编写一个模版方法,该模版方法接受一个容器对象和测试时运行的迭代次数,这就是本次问题的核心。要实现这个功能有多种方法,先看不使用模版模版参数以及模版别名的实现方式。
方法一:使用方法模版、迭代器以及迭代器特性来实现
我们在获取一个STL容器实例对象之后,如何获取该容器里面的元素类型呢?拜STL中所有的容器都实现了迭代器(iterator)这一特性所赐,我们可以使用迭代器特性(iterator_traits)来从容器的迭代器中获取元素的具体类型,这样就可以对这个获取到的类型进行实例化,这是整个方法的关键。
#include <typeinfo>
#include <iterator>
#include <iostream>
template <typename T>
void output_static_data(const T &mystr)
{
std::cout << typeid(mystr).name() << "--" << std::endl;
std::cout
<< "CCtor=" << T::CCtor
<< " MCtor=" << T::MCtor
<< " CAssign=" << T::CAssign
<< " MAssign=" << T::MAssign
<< " DCtor=" << T::DCtor
<< " Ctor=" << T::Ctor
<< " Dtor=" << T::Dtor
<< std::endl;
}
template <typename M, typename NM>
void test_moveable(M c1, NM c2, long &value)
{
char buf[10];
// test move
typedef typename std::iterator_traits<typename M::iterator>::value_type V1Type;
std::cout << "\n\ntest, with moveable elements" << std::endl;
clock_t timeStart = clock();
for (long i = 0; i < value; ++i)
{
snprintf(buf, 10, "%d", rand());
auto ite = c1.end();
c1.insert(ite, V1Type(buf));
}
std::cout << "construction,milli-seconds : " << (clock() - timeStart) << std::endl;
std::cout << "size()=" << c1.size() << std::endl;
output_static_data(*(c1.begin()));
timeStart = clock();
M c11(c1);
std::cout << "copy, milli-seconds : " << (clock() - timeStart) << std::endl;
timeStart = clock();
M c12(std::move(c1));
std::cout << "move copy, milli-seconds : " << (clock() - timeStart) << std::endl;
timeStart = clock();
c11.swap(c12);
std::cout << "swap, milli-seconds : " << (clock() - timeStart) << std::endl;
// test nomove
typedef typename std::iterator_traits<typename NM::iterator>::value_type V2Type;
std::cout << "\n\ntest, with no-moveable elements " << std::endl;
.........//代码同上,此处省略
}
上面这个模版方法有两个模版参数M和NM,用户可以传入同一种容器类型的两种不同元素,比如像下面这样调用:
long value = 1000000;
test_moveable(std::vector<MyString>(), std::vector<MyStringNoMove>(), value);
方法在获取到了模版参数之后,通过使用迭代器特性(iterator_traits)来查询容器迭代器中的数据类型,得到了V1Type,然后通过for循环,随机生成字符串,最后通过容器的插入方法,并使用迭代器,将元素插入到容器中。方法后面的部分分别测试了拷贝构造,移动拷贝构造等的耗时时间。上面方法的运行结果如下:
test, with moveable elements
construction,milli-seconds : 1440
size()=1000000
8MyString--
CCtor=0 MCtor=2048575 CAssign=0 MAssign=0 DCtor=0 Ctor=1000000 Dtor=2048575
copy, milli-seconds : 550
move copy, milli-seconds : 0
swap, milli-seconds : 0
test, with no-moveable elements
construction,milli-seconds : 100884
size()=1000000
14MyStringNoMove--
CCtor=2048575 MCtor=0 CAssign=0 MAssign=0 DCtor=0 Ctor=1000000 Dtor=2048575
copy, milli-seconds : 427
move copy,milli-seconds : 0
swap,milli-seconds : 0
可以看到,对于vector这种会自动扩容的容器,容器中的对象是否具有移动构造和移动赋值,对程序性能的影响很大。比如在初始化插入对象的时候,如果对象有移动构造函数,那么就直接调用移动构造函数,如果没有,就需要先调用拷贝构造函数生成临时对象,然后将该对象调用拷贝构造函数放到容器中,然后调用该临时对象的析构函数。可以看到,单单在构造阶段的速度差了将近100倍。
方法二:使用模版模版方法和模版别名来实现
上面的方法中,是使用的迭代器特性来从模版参数中查询到的容器里面元素的类型,假如某个容器不支持迭代器,或者说没有实现迭代器特性呢。那么就没有办法使用以上方法了。
那能不能将容器中的元素类型作为模版参数传进去呢?接下来就是几种尝试
尝试一:普通方法
第一种尝试就是使用普通的方法,将容器名称和元素类型直接传进去:
void test_move(Container c, T element, long &value)
{
Container<T> c;
for (long i = 0; i < value; ++i)
{
c.insert(c.end(), T());
output_static_data(T());
}
Container<T> c1(c);
Container<T> c2(std::move(c));
c1.swap(c2);
}
然后使用如下方法调用:
test_move(std::vector,MyString,100000);
test_move(std::vector,MyStringNoMove,100000);
但这只是异想天开,以上代码都无法通过编译:
错误有很多,比如没有定义Container类型,传进去的std::vector跟Containner没有任何关系,T类型也没有定义,因为这是一个普通的方法,而不是模版方法。
将该方法改为模版方法,这就是方案一的解决方案。
template<typename Container>
void test_move(Container c, long &value)
{
typedef typename std::iterator_traits<typename Container::iterator>::value_type ValType;
for (long i = 0; i < value; ++i)
{
c.insert(c.end(), ValType());
output_static_data(*(c.begin()));
}
Container c1(c);
Container c2(std::move(c));
c1.swap(c2);
}
使用如下:
test_move(std::vector<MyString>(),value);
test_move(std::vector<MyStringNoMove>(),value);
尝试二:模版模版参数方法
还是希望尝试走前面那条路,如果模版参数本身就是一个模版,有没有办法获取这个模版模版参数的类型呢?这个就是模版模版参数的用法:
template <template <class> class Container, typename T>
class XCls
{
private:
Container<T> c;
public:
XCls(long &value)
{
for (long i = 0; i < value; ++i)
{
c.insert(c.end(), T());
output_static_data(T());
}
Container<T> c1(c);
Container<T> c2(std::move(c));
c1.swap(c2);
}
};
省略了一些计时的操作。这段代码相当完美,这应该就是我们想要的,并且能编译通过。用法如下:
XCls<std::vector,MyString> c1(value);
这段语句也能运行,结果如下:
8MyString--
CCtor=0 MCtor=2048575 CAssign=0 MAssign=0 DCtor=1000001 Ctor=0 Dtor=2048575
但在候捷老师的课里,这里能编译成功,但是运行的时候是会报错的🤣,他说原因在与标准模板库的定义中除了定义元素类型T之外,还有一个分配器参数Allocator,其中的类型也是T。比如std::vector的签名如下:
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
{
}
这个模版类有两个参数,且第二个模版参数的类型依赖于前一个模版参数,虽然第二个模版参数有默认值,但在候捷老师上课的那个时候,编译器是会报错的,不能直接给vector提供一个参数,因为第二个默认的参数没有办法推导出第一个模版参数,故而编译器无法推导出它的分配器,分配器必须显示声明。
当然奇怪的是,在我这里是可以运行且能成功的。我们假装不成功,看接下来的解决方法。
尝试三:模版模版参数和模版别名
在候捷老师当时的编译器下,标准模版库的第二个默认的分配器类型无法获取第一个模版参数,编译器无法推导出它的分配器,所以需要用到模版别名(alias template),就是使用using关键字。
template <typename T>
using Vec = std::vector<T, std::allocator<T>>;
template <typename T>
using Lst = std::list<T, std::allocator<T>>;
template <typename T>
using Deq = std::deque<T, std::allocator<T>>;
这里将Vec指定为了vector的别名,注意这里的模版参数是未固定的,接下来使用Vec<int>相当于使用了std::vector<int,std::allocator<int>>,这是模版别名不同于typedef的地方,typedef不支持模板化,但模板别名可以。
有了以上模版别名,尝试二里面定义的模版模版参数就能正常运行,调用方式变为了如下:
XCls<Vec,MyString> c1(value);
XCls<Lst,MyString> c2(value);
XCls<Deq,MyString> c3(value);
完美解决问题。
参考:
- https://www.cnblogs.com/xiaoxiaolinux/p/14165025.html
- 《Effective Modern C++》,Item 9
- 《C++新标准C++11&14》,侯捷