《Effictive C++》学习笔记 —模板与泛型编程
条款41 — 了解隐式接口和编译期多态
1、隐式接口和编译期多态
这两个概念听起来高大上,实际上我们早就在接触它们了。将它们与另外一组概念对比起来很好记:
显式接口指的是出现在类定义中的类方法定义。这类接口显式地规定了类方法的参数及返回值等信息。当它们被声明为虚方法时,它们所表现的特性被称为运行时多态。
类似地,隐式接口指的是在模板元编程中,以类型参数(即typename)所修饰的对象执行的操作及表现的行为。在声明模板类或模板方法时,我们并不知道它们会被以什么样的模板参数实例化。但是,我们可以对模板参数类型所需要支持的方法隐式地提出要求。不满足相应行为的模板参数类型将无法通过编译。当我们使用不同的参数去初始化模板时,在编译期会导致不同的具现化以及调用不同的方法同名。这种特性便是所谓的编译期多态。
#include <iostream>
using namespace std;
template<typename T>
bool test(T& obj)
{
return obj << 123 && obj << 456;
}
int main()
{
int i = 5;
test(cout);
test(i);
}
对于模板方法test来说,它貌似要求obj对象支持 << 操作符以及bool类型转换函数,这便是隐式接口。而对于传入int引用和ostream引用,test在屏幕上的输出是不一样的。这种不同是在编译器就确定下来的,体现的是编译期多态。
2、有效表达式
显式接口和隐式接口的区别是:显式接口是基于函数签名式的,而隐式接口是基于有效表达式的。这是什么意思呢?显式接口规定的是调用方调用某一接口时传参和保存返回值的变量类型,即参数必须能隐式转换为接口所支持的类型;返回值至少要满足返回类型协变。而隐式接口的要求是涉及到模板参数的表达式是有效的。从某种意义上讲,这种规范更为宽泛,它包括但不限于前者的规定。例如,显式接口等同于规定了只能调用类方法或友元方法;而隐式接口却可以调用普通函数(以操作符重载为主)。
条款42 — 了解typename的双重意义
typename关键字最常用的地方就是定义模板时位于模板函数或模板类声明前,用于指定模板参数别名。在《深度探索C++对象模型》中,作者提到,在这种使用条件下,class和typename并无区别,他们的区别仅在于模板编写者的习惯或者暗示。
然而在另一种使用场景下,typename是无法被替代的。
1、从属名称和非从属名称
考虑以下的例子(来源书中):
template<typename T>
void print2nd(const T& container)
{
if (container.size() > 2)
{
T::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
这里我们需要区别两个变量value和iter。显然,value的类型不会随着模板参数的改变而改变,其类型是int。像这种出现在模板中而不会随模板参数而发生变化的的名称,被称为非从属名称(还是英语好记,non-dependent names)。
与之相比,iter的类型却有赖于模板特化。像C::const_iterator这种出现在模板中而且依赖于某个模板参数的类型名称(或者从编写者的角度认为其是个类型名称),被称为从属名称(dependent names)。如果从属名称在class内部呈嵌套状,就被称为嵌套从属名称(nested dependent names)。如果该名称表示一种类型,它就是嵌套从属类型名称(nested dependent type names)。
2、非从属名称
非从属名称的调用绑定是在模板被定义时发生的。即使在模板被实例化时有一个更好的匹配选择,也不会改变调用。
#include <iostream>
using namespace std;
void test(double)
{
cout << "double" << endl;
}
template<typename T>
class CLS_Test
{
public:
void innerTest()
{
test(1);
}
};
void test(int)
{
cout << "int" << endl;
}
int main()
{
CLS_Test<double> obj;
obj.innerTest();
}
3、嵌套从属名称
从属名称的绑定发生于模板实例化。这里我们直接讨论嵌套从属名称与typename的关系。C++参考手册中给出:对于嵌套从属名称,只有被typename关键字修饰或者使用于typedef时才会被解析为类型名;在模板中,typename只应该出现在嵌套从属名称前面。所以,我们上面给出的书中的例子其实是编译不过的。因为那个嵌套从属类型名称根本没有被认为是类型名称。下面是修改的办法:
typename T::const_iterator iter(container.begin());
如果不想多次声明这个类型,我们也可以使用typedef:
typedef typename T::const_iterator iterType;
C++参考除了规定了嵌套从属名称被解析为类型的条件,也提出了一些例外情况。我们这里列举出几个,包括书中列出的两种:
#include <iostream>
using namespace std;
template<typename T>
class CLS_Base
{
public:
class CLS_BaseInner
{
};
};
template<typename T>
class CLS_Derived : public CLS_Base<T>::CLS_BaseInner // used in base classes list
{
CLS_Derived():
CLS_Base<T>::CLS_BaseInner() // used in member initialization list
{
}
CLS_Base<T> m_tMember; // used in class member declaration
void test(CLS_Base<T> _para) // used in function
{
// CLS_Base<T>::CLS_BaseInner innerObj; invalid
}
};
归根结底,C++的判断准则就是当无法确定一个从属类型名称的作用时,如果编写者希望它被认为是一个参数类型,需要显式地指出这一点。
条款43 — 学习处理模板化基类内的名称
1、模板类继承中的问题
假设我们需要编写一个程序发送信息到不同的公司。如果在编译期我们就可以确定具体是哪家公司,而且所有公司发送信息的接口是同名的,我们就可以使用模板实现此功能:
class CLS_MsgInfo
{
public:
const string getMsg()
{
return "testLog";
}
};
template<typename Company>
class CLS_MsgSender
{
public:
void sendMsg(const CLS_MsgInfo& info)
{
string msg = info.getMsg();
Company c;
c.sendText(msg);
}
};
如果我们想扩展此类,增加些日志的功能,我们可以使用继承:
template<typename Company>
class CLS_LogMsgSender : public CLS_MsgSender<Company>
{
public:
void sendMsgWithLog(const CLS_MsgInfo& info)
{
// write log
sendMsg(info);
// write log
}
};
然而这样是无法通过编译的。理由我们在上一个条款中提到过:非从属名称的调用绑定是在模板被定义时发生的。也就是说,sendMsg函数与模板参数无关,因此在CLS_LogMsgSender定义时,编译器就会去找和它匹配的名称。这时由于其基类没有具体化,因此在代码中根本找不到一个合适的名称匹配!
作者在书中的解释略有些牵强。因为这时我们只要把CLS_MsgInfo由一个类定义转化为模板参数就可以编译通过。在这种情况下,我们同样不知道CLS_LogMsgSender的基类是否有sendMsg,但是编译却没有问题。这还是归根于C++的名称查找规则。
template<typename Company, class CLS_MsgInfo>
class CLS_LogMsgSender : public CLS_MsgSender<Company>
{
public:
void sendMsgWithLog(const CLS_MsgInfo& info)
{
// write log
sendMsg(info);
// write log
}
};
2、解决方案
共有三种解决方案,其本质都是把该名称变为从属名称。
(1)增加this指针
void sendMsgWithLog(const CLS_MsgInfo& info)
{
this->sendMsg(info);
}
(2)使用using声明式
void sendMsgWithLog(const CLS_MsgInfo& info)
{
using CLS_MsgSender<Company>::sendMsg;
sendMsg(info);
}
(3)使用类作用域限定符
void sendMsgWithLog(const CLS_MsgInfo& info)
{
CLS_MsgSender::sendMsg(info);
}
作者针对最后一种解法,提出其问题在于虚函数的动态调用将会变为静态调用。这点我个人不是很理解。因为在继承体系中,即使对于虚函数,我们在子类调用时也能明确地知道该调用的是哪个函数,而不需要依赖虚函数的动态绑定。后两种解法确实都存在静态绑定的情况。但我认为这种静态绑定带来的问题是:位于非直接父类中的方法将不能被调用。这个问题和虚函数没有关系。如果朋友们知道第三种使用导致的虚函数问题,请帮忙指出。下面我用代码示例下我所说的问题:
#include <iostream>
using namespace std;
class CLS_MsgInfo
{
public:
const string getMsg() const
{
return "testLog";
}
};
class CLS_MyCompany
{
public:
void sendText(const string& msg)
{
cout << "msg" << endl;
}
};
template<typename Company>
class CLS_MsgSender
{
public:
virtual void sendMsg(const CLS_MsgInfo& info)
{
string msg = info.getMsg();
Company c;
c.sendText(msg);
}
};
template<typename Company>
class CLS_MsgMiddleSender : public CLS_MsgSender<Company>
{
};
template<typename Company>
class CLS_LogMsgSender : public CLS_MsgMiddleSender<Company>
{
public:
void sendMsgWithLog(const CLS_MsgInfo& info)
{
sendMsg(info);
}
};
int main()
{
CLS_LogMsgSender<CLS_MyCompany> sender;
const CLS_MsgInfo info;
sender.sendMsgWithLog(info);
}
在这种情况下,后两种解法在不实例化的时候可以编译通过,但是无法通过增加模板实例化的代码测试。
条款44 — 将与参数无关的代码抽离templates
1、代码膨胀的例子
我们将模板参数分为三类:non-type模板参数(一般为常量)、type模板参数(定义时即可的类型)、template模板参数(实例化确定的类型)。当我们使用non-type模板参数时,很容易造成代码的膨胀。考虑一个计算逆矩阵的类:
template <typename T, size_t n>
class CLS_SquareMatrix
{
public:
void invert();
};
int main()
{
CLS_SquareMatrix<int, 5> squMat5;
CLS_SquareMatrix<int, 10> squMat10;
CLS_SquareMatrix<int, 15> squMat15;
}
我们在学习《C++ Primer Plus》时已经知道,这样会为我们生成多个模板实例。我们需要注意,这里模板的实例化无关参数是否被使用。即我们即使只声明了一个模板参数而未使用,那么如果在实例化的时候此参数是不同的,也将为类模板生成不同的具体化实例。
2、抽离共性代码
现在两个模板类之间唯一的不同就是non-type模板参数。不同的参数会导致不同的实例化中出现重复的invert函数体。此时,我们想到的方案应该是将抽离这部分代码。对于函数,重复的代码将会被封装成子函数调用;那么对于类方法呢?显然是放到基类中。
template <typename T>
class CLS_SquareMatrixBase
{
public:
void invert(int n);
};
template <typename T, size_t n>
class CLS_SquareMatrix : private CLS_SquareMatrixBase<T>
{
private:
using CLS_SquareMatrixBase<T>::invert;
public:
void invert()
{
this->invert(n);
}
};
这样我们就把相同的代码抽取到基类中,而且基类仅负责数据处理而不负责数据存储。因此我们可以将矩阵大小作为参数传递给它。我们简化代码的代价是为不同参数类型的矩阵生成了不同的基类。但是在需要大量不同n实例化的CLS_SquareMatrix情况中,这样再好不过了。
这里我们需要注意继承关系为私有继承,因为我们创建CLS_SquareMatrixBase类的目的在于借助它实现功能。除此之外,我们可以发现这里既使用了using声明式又使用了this指针,是否为重复呢?这里using声明的作用是防止基类的函数被派生类的同名函数覆盖导致无法访问(我们在条款33讨论过这点);同时它也可以起到我们条款43所说的防止定义时寻找声明的功能。而作者提出this指针起到此作用,我觉得可能有些多余。
3、数据访问
上述讨论基于一个事实,数据的存储位于派生类而共性代码放置于基类中。那么问题来了,基类的方法如何访问派生类的数据呢?我们可以考虑以下几种方法:在invert函数增加参数传递内存指针;在基类中保存内存指针及数组长度。无论哪种方式,都可以由派生类决定数据的存储方式(堆or栈)。
class CLS_SquareMatrixBase
{
private:
T* pData;
size_t size;
public:
CLS_SquareMatrixBase(T* _pData, size_t _size) :
pData(_pData),
size(size)
{
}
}
这里我们需要明确:这里的目的只是将生成的代码减少。在运行时,每个派生类实例的基类部分都是独立的,不会因为只有一份代码就共享基类内存。因此将数据指针和矩阵大小保存到基类并不会导致数据冲突。
4、其他讨论
我们确实减小了代码量。但是我们还需要更充分地评估这一优化。一方面,non-type模板参数在编译器作为常数,生成汇编代码时可以作为直接操作数;函数调用却必须付出压栈和寄存器保存的代价;相比而言,我们得到的好处是执行文件大小的减小,因此可以降低程序的内存页大小,强化指令高速缓存区的引用集中化(也就是提高指令在缓冲区的命中率)。另一方面,从生成的对象大小来说,基类版本显然会耗费更大的内存。我们要保存数据指针。当然,这是个可以优化的点,我们可以将基类做成接口类。
除了non-type模板参数,type模板参数同样会引发代码膨胀。例如,使用int和long具现化的模板类可能在底层具有完全相同的功能。使用不同类型指针具现化的模板类也会增加代码的数量。这正是这个条款教会我们去做的。观察那些与参数无关的代码,抽离出它们并放到实现类中。
条款45 — 运用成员函数模板接受所有兼容类型
考虑智能指针模板类。我们前面讨论过如何使用其进行资源管理。这里我们主要讨论如何在以基类和派生类实例化的模板类实分别实例化的模板对象如何相互转化。
#include <iostream>
using namespace std;
class CLS_Base{};
class CLS_Derived : public CLS_Base{};
int main()
{
shared_ptr<CLS_Derived> dPtr(new CLS_Derived);
shared_ptr<CLS_Base> bPtr = dPtr;
}
这样的代码可以执行通过。然而,我们知道,尽管CLS_Base和CLS_Derived为父子类关系,它们实例化的模板类却没有这样的关系。那么它们是怎样相互转化的呢?
我们第一想法肯定是实现了对应的拷贝构造函数。然而,模板类的作者都不知道它们将被怎样的类实例化,也不知道这些类有怎样的继承体系,怎么编写出具体的拷贝函数呢?这时我们自然会想到使用模板函数。它看起来像是这个样子:
template <typename T>
class CLS_SmartPtr
{
public:
template <typename U>
CLS_SmartPtr(const CLS_SmartPtr<U>& other);
};
这个函数有时被称为泛化构造函数。这里我们没有使用explicit关键字是为了支持显式转换。
那么我们如何限制U和T的关系呢?显然,我们需要借助被管理的指针自身。虽然模板是泛化的,但是被管理的对象之间的转化关系却是确定的:
template <typename T>
class CLS_SmartPtr
{
public:
template <typename U>
CLS_SmartPtr(const CLS_SmartPtr<U>& other) :
m_pData(other.get())
{
cout << "generalized copy" << endl;
}
T* get() const
{
return m_pData;
}
private:
T* m_pData;
};
当然,使用在拷贝函数上的时候很多时候都可以被应用于赋值操作符重载上。
现在我们看看msvc中该函数的实现:
template <class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
shared_ptr(const shared_ptr<_Ty2>& _Other) noexcept {
// construct shared_ptr object that owns same resource as _Other
this->_Copy_construct_from(_Other);
}
_SP_pointer_compatible使用的是编译器的内置函数 __is_convertible_to 判断转换关系。这是一个编译期行为。
最后,我们还需要看看泛化拷贝构造函数是否会影响默认拷贝构造函数的生成。按照C++的规则,模板函数在被调用之前并不会被实例化。而默认的拷贝构造函数是什么时候生成的呢?在编译器看到类定义时生成的。那编译器是什么时候看到类定义的呢?在类模板被实例化的时候。正常情况下,类肯定是先被实例化的,然后才是类中的非约束模板函数被实例化。我们写代码验证下:
#include <iostream>
using namespace std;
template <typename T>
class CLS_SmartPtr
{
public:
CLS_SmartPtr(T* _pData) :
m_pData(_pData)
{
}
template <typename U>
CLS_SmartPtr(const CLS_SmartPtr<U>& other) :
m_pData(other.get())
{
cout << "generalized copy" << endl;
}
T* get() const
{
return m_pData;
}
private:
T* m_pData;
};
class CLS_Base {};
class CLS_Derived : public CLS_Base {};
int main()
{
CLS_SmartPtr<CLS_Derived> dPtr1(new CLS_Derived);
cout << "first copy" << endl;
CLS_SmartPtr<CLS_Derived> dPtr2(CLS_SmartPtr<CLS_Derived>(new CLS_Derived));
cout << "second copy" << endl;
CLS_SmartPtr<CLS_Base> dPtrCopy = dPtr2;
}
shared_ptr(const shared_ptr& _Other) noexcept { // construct shared_ptr object that owns same resource as _Other
this->_Copy_construct_from(_Other);
}
参考msvc实现,我们可以发现它确实实现了两种拷贝构造函数,并且将共同代码封装。因此,如果我们想控制模板类的各种拷贝构造,需要同时实现泛化构造函数和普通构造函数。
条款46 — 需要类型转换时请为模板定义非成员函数
1、模板类的操作符重载问题
考虑条款24中的运算符重载例子,现在我们把其改为模板类及对应的函数:
template<typename T>
class CLS_Test
{
private:
int m_iMem;
public:
CLS_Test(int _iPara)
{
m_iMem = _iPara;
}
};
template<typename T>
const CLS_Test<T> operator+(const CLS_Test<T>&lhs, const CLS_Test<T>& rhs)
{
return CLS_Test<T>(lhs.m_iMem + rhs.m_iMem);
}
int main()
{
CLS_Test<int> test(1);
CLS_Test<int> testLhs = test + 1;
}
上述的代码将无法通过编译。显然,这和非模板函数的情况不一致。从编译器的角度看,当遇到 + 操作符时,它首先尝试找到一个最佳匹配版本,然后才会去尝试具体化模板函数(相关的函数模板具现化优先级可以参考我复习基础知识提到的《C++ Primer Plus》学习笔记 — 函数知识补充)。然而,它并没有找到。虽然我们提供了从int到CLS_Test的隐式转换函数,但是编译器在推导模板的时候并不会考虑隐式转换函数。因此,就算我们在函数模板后面增加一个显式实例化函数编译仍旧不会通过。
C++手册中提到了涉及到模板函数编译的过程,我们这里简单说下:首先是名称查找,查找最匹配的名称;然后是模板参数推导,推导使用什么样的参数具体化模板;最后是重载解析。隐式转换函数的匹配是发生在重载解析过程中的。因此在推导过程中当然不会考虑它。当然,手册中也提到(作为书中提到知识的补充),如果只有非从属名称无法推导,将会考虑隐式函数转换。因此我们把重载函数改一下就可以编译通过(这里的第二个参数的模板类型被指定):
template<typename T>
const CLS_Test<T> operator+(const CLS_Test<T>&lhs, const CLS_Test<int>& rhs){}
但是,这显然并不是我们想要的结果。
2、使用重载的友元版本
这时,我们就会想到重载操作符除了可以作为普通函数,还可以作为友元函数。同时,模板类的友元函数中有一种叫做模板类的约束模板友元函数。其具体化与模板类的具体化一同发生,且二者的模板参数相同。
template<typename T>
class CLS_Test
{
...
template<typename T>
friend const CLS_Test operator+(const CLS_Test& lhs, const CLS_Test& rhs);
}
template<typename T>
const CLS_Test<T> operator+(const CLS_Test<T>& lhs, const CLS_Test<T>& rhs)
{
return CLS_Test<T>(lhs.m_iMem + rhs.m_iMem);
}
然而,这样仍然无法通过编译。这次产生的错误是无法链接到函数。为什么呢?因为友元函数被具现化了而类外的模板函数却没有具现化。因此是找不到对应的定义的。因此我们最好把这个友元函数定义为隐式内联函数。
template<typename T>
class CLS_Test
{
...
friend const CLS_Test operator+(const CLS_Test& lhs, const CLS_Test& rhs)
{
return CLS_Test<T>(lhs.m_iMem + rhs.m_iMem);
}
};
如果需要,我们可以把 + 操作封装成函数放在类头文件中。
条款47 — 请使用traits classes表现类型信息
考虑STL中的一个函数advance,用于将迭代器向前移动给定距离:
template <class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off)
这隐式要求了我们所使用的迭代器的种类和处理方式:对于随机访问迭代,使用 += 和 -= 操作符;对于双向迭代器,反复使用 ++ 和 - - 操作符;对于其他操作符,则仅使用使用 ++ 操作符支持正向的移动。对于不同种类的迭代器,C++提供了五种卷标结构加以确认:
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : input_iterator_tag {};
struct bidirectional_iterator_tag : forward_iterator_tag {};
struct random_access_iterator_tag : bidirectional_iterator_tag {};
那么我们这里肯定希望能通过某种类型信息在编译期确定所使用的迭代器是否是满足我们需要的类型:
template <class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off)
{
if (_Where is random iterator)
{
_Where += _Off;
}
else if (_Where is bidirectional iterator)
{
...
}
else
{
...
}
}
1、iterator_traits
我们可以使用traits来实现这种功能。Traits并不是C++的关键字或预定义的构件:它们是一种技术,也是一种协议规范。这种技术的要求之一是,对内置类型和用户自定义类型的表现必须一样好。也就是说如果我们传给advance函数的参数是指针,该函数仍能有效运作。这意味着什么呢?我们不能使用类型内的嵌套信息(也就是不能在类型内定义type属性用于标识自身类型)。因此类型信息必须独立于类信息存在。标准技术是把它放进一个模板及其特化版本中。针对迭代器的模板被命名为iterator_traits。
template <class _Iter>
struct iterator_traits : _Iterator_traits_base<_Iter> {}; // get traits from iterator _Iter, if possible
这个模板类的普通版本象征非迭代器,我们使用不同的特化版本表示不同类型的迭代器。通常情况下,我们需要特化五个属性:
class _Vector_const_iterator : public _Iterator_base {
public:
#ifdef __cpp_lib_concepts
using iterator_concept = contiguous_iterator_tag;
#endif // __cpp_lib_concepts
using iterator_category = random_access_iterator_tag;
using value_type = typename _Myvec::value_type;
using difference_type = typename _Myvec::difference_type;
using pointer = typename _Myvec::const_pointer;
using reference = const value_type&;
...
}
iterator_category定义了迭代器的属性。我们可以看到vector迭代器的属性正是随机访问迭代器。针对自定义类型的iterator_traits正是利用了这个属性:
template <class _Iter>
struct _Iterator_traits_base<_Iter,
void_t<typename _Iter::iterator_category, typename _Iter::value_type, typename _Iter::difference_type,
typename _Iter::pointer, typename _Iter::reference>> {
// defined if _Iter::* types exist
using iterator_category = typename _Iter::iterator_category;
...
};
针对于指针类型,其又做了不同的特化:
template <class _Ty, bool = is_object_v<_Ty>>
struct _Iterator_traits_pointer_base { // iterator properties for pointers to object
using iterator_category = random_access_iterator_tag;
using value_type = remove_cv_t<_Ty>;
using difference_type = ptrdiff_t;
using pointer = _Ty*;
using reference = _Ty&;
};
template <class _Ty>
struct iterator_traits<_Ty*> : _Iterator_traits_pointer_base<_Ty> {};
is_object_v用于判断是否为对象。我们可以看出指针被设置为随机访问迭代器。
参考上面iterator_traits,我们知道设计一个trait class需要分为以下几步:
(1)确认我们需要的类型信息,例如对于迭代器我们希望取得其分类(category),有时需要获取其数据类型(value_type);
(2)为该信息选择一个名称,例如iterator_category;
(3)提供一个模板和一组特化版本,内含你希望支持的类型相关信息。
2、advance的实现
现在我们有了iterator_traits,首先我们会想到通过判断category实现advance:
template <class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off)
{
if (typeid(typename std::iterator_traits<_InIt>::iterator_category) == typeid(std::random_access_iterator_tag))
...
}
即使忽略编译可能无法通过的问题,我们仍旧失去了在编译期静态确定类型的功能。我们需要寻找编译器的if-else条件判断。这项工具我们都很熟悉,它就是重载。
template <class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off)
{
doAdvance(_Where, _Off, std::iterator_traits<_InIt>::iterator_category));
}
template <class _InIt, class _Diff>
_CONSTEXPR17 void doAdvance(_InIt& _Where, _Diff _Off, std::random_access_iterator_tag);
template <class _InIt, class _Diff>
_CONSTEXPR17 void doAdvance(_InIt& _Where, _Diff _Off, std::bidirectional_iterator_tag);
template <class _InIt, class _Diff>
_CONSTEXPR17 void doAdvance(_InIt& _Where, _Diff _Off, std::input_iterator_tag);
这样的实现还有一个潜在的好处:我们可以利用迭代器tag struct的继承关系。因此,上述处理input_iterator_tag的函数版本也能处理forward_iterator_tag。这再好不过了。
综上,我们可以看出如何使用一个trait class:
(1)建立一组重载函数用于处理不同traits参数;
(2)建立一个控制函数用于调用上述重载函数并控制traits参数。
我们也许可以这种方式称为工厂模式的函数版本。
条款48 — 认识模板元编程
所谓模板元编程是以 C++ 写成,执行与编译器内部的程序。其作用体现在:简化代码;将部分程序员的编码工作和运行时工作转移到编译期进行。这使得运行中的错误减少,同时使得程序更加高效。当然,我们同样付出了更长的编译时间。
1、advance遗留的问题
回顾下我们设计advance函数的中间版本:
template <class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off)
{
if (typeid(typename std::iterator_traits<_InIt>::iterator_category) == typeid(std::random_access_iterator_tag))
{
_Where += _Off;
}
else
{
...
}
}
这样的问题在于只有传参为随机访问迭代器分类的迭代器对象时代码才能编译通过。因为我们使用的if-else为运行期检查。也就是说,我们需要保证当if条件不满足时,_Where += _Off 仍可以编译通过。而 += 操作符正是随机访问迭代器的专属。
2、TMP的循环
TMP中并不存在直接支持循环的语法,这项功能是借由递归完成的。例如计算阶乘的模板类:
#include <iostream>
using namespace std;
template<unsigned int n>
struct Factorial
{
enum {value = n * Factorial<n - 1>:: value};
};
template<>
struct Factorial<0>
{
enum { value = 1 };
};
int main()
{
cout << Factorial<5>::value << endl;
}
是的,我们完全可以在编译期计算确定数字的阶乘!
3、模板的目标
在我们尝试使用TMP之前,我们要先了解其目标,下面我们列举其中几项:
确保度量单位正确
优化矩阵运算
定制设计模式
毕竟这本书关注的也不是模板本身,因此我们这里就不再探索模板了。