条款41-48

本文探讨了C++模板编程的关键概念和技术,包括隐式接口、编译期多态、typename的作用、处理模板化基类内的名称等。介绍了成员函数模板如何接受兼容类型,以及何时应定义非成员函数进行类型转换。此外,还讲解了traits类和模板元编程的基本原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款41:了解隐式接口和编译期多态

        面向对象编程总是以显示接口和运行期多态解决问题。template及泛型编程的世界,与面向对象有根本不同,虽然显示接口和运行期多态仍然存在,但重要性降低,而隐式接口和编译期多态更为重要。

        运行期多态和编译期多态类似于“哪一个重载函数被调用”(编译期)和“哪一个virtual函数该被绑定”(运行期)的差异类似。

        显示接口由函数签名式(名称、参数类型、返回类型)构成,隐式接口不基于函数签名式,而是由函数内部有效表达式所决定的。

        加诸于template参数身上的隐式接口,就像加诸于class对象身上的显示接口一样真实,而且两者都在编译期完成检查。就像你无法以一种“与class提供的显示接口矛盾”的方式来使用对象,你也无法在template中使用“不支持template所要求的隐式接口”的对象。

请记住:

        class和template都支持接口和多态

        对class而言接口是显示的,以函数签名为中心,多态则是通过virtual函数发生于运行时期

        对template参数而言,接口是隐式的,奠基于有效表达式,多态则是通过template具现化和函数重载解析发生于编译期


条款42:了解typename的双重意义
template<typename C> void print2nd(const C &container)
{
if(container.size()>=2)
{
C::const_iterator iter(container.begin());
++iter;
int value=*iter;
std::cout<<value;
}
}

        分析上述打印STL容器第二个元素值的函数为何不能通过编译。template内出现的名称如果依赖于某个template参数,则称之为从属名称,如果从属名称在class内呈嵌套状,称之为嵌套从属名称。C::const_iterator就是这样一个名称。实际上它还是一个嵌套从属类型名称,也就是嵌套从属名称并且涉指某各类。对于value,其类型是int,称为非从属名称

        嵌套从属名称可能导致解析困难,我们可以在函数开头编写:

C::const_iterator *x;

        看起来似乎是声明了一个C::const_iterator的指针,之所以这么认为,是由于我们知道C::const_iterator是一个类型,但是如果C中有一个static成员变量名称是const_iterator,上述代码就是变为相乘操作。并且解析器自动将嵌套从属名称假设为非类型

        因此任何时候当想要在template中涉指一个嵌套从属类型名称,必须在紧邻它的前一个位置放上关键字typename:

template<typename C> void print2nd(const C &container)
{
if(container.size()>=2)
{
typename C::const_iterator iter(container.begin());
...
}
}

        typename只被用来验明嵌套从属类型名称,这一规则的例外是:不可出现在base class list内的嵌套从属类型名称之前,也不可在member initialize list(成员初始值列)中作为base class修饰符:

template<typename T> class Derived
:public Base<T>::Nested{            //base class list中不允许“typename”
public:
explicit Derived(int x)
:Base<T>::Nested(x)                 //member initialization list中不允许“typename”
{
typename Base<T>::Nested temp;      //嵌套从属类型名称允许“typename”
...
}
};
请记住:

        声明template参数时,前缀关键字class和typename可互换

        请使用关键字typename标识嵌套从属类型名称;但不可以在base class list或member initialization list内作为base class修饰符


条款43:学习处理模板化基类内的名称
template<typename Company> class MsgSender{
public:
void sendClear(...){...}
void sendSecret(...){...}
};
template<typename Company> class LoggingMsgSender:public MsgSender<Company>{
public:
void sendClearMsg(...)
{
...
sendClear()            //调用基类函数,此处不能通过编译
...
}
};

        之所以不能通过编译,是由于编译器 不知道LoggingMsgSender继承的是什么样的class,因为base class是一个template class,编译器不知道该base class是否有sendClear函数。例如,我们可以产生一个特化模板:

template<> class MsgSender<CompanyZ>{
public:
void sendSecret(...){...}
};

        由此可见,当company==companyZ,sendClear函数将不存在,这就是为什么编译不通过的原因:base class template有可能被特化,而那个特化版本不一定会提供和一般template一样的接口。因此编译器拒绝在templatized base class(模板化基类)内寻找继承而来的名称

        为了解决“不进入templatized base classes观察”有三种办法:

  • 在base class函数的调用动作之前加上this->,假设会继承基类的sendClear函数:
void sendClearMsg(...)
{
this->sendClear(...);
}
  • 使用using声明:
using MsgSender<Company>::sendClear;        //假设sendClear位于base class内
void sendClearMsg(...)
{
sendClear(...);
}
  • 明白指出被调用的函数位于base class内:
void sendClearMsg(...)
{
MsgSender<Company>::sendClear(...);
}

        以上三种办法均可解决上述问题,但第三种办法相较而言不够完美,会关闭virtual绑定行为。虽然通过编译,但如果上述承诺未被实践,编译器仍然会报错:

LoggingMsgSender<CompanyZ> zMsgSender;
zMsgSender.sendClearMsg(...);        //无法通过编译

        因为此时编译器已经知道base class是特化版本MsgSender<CompanyZ>,并且该特化版本不提供sendClear函数

请记住:

        可在derived class templates内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class”资格修饰符完成

/*        条款44还没有很好的理解,就先略过了,后续有所领悟再回头修改        */


条款45:运用成员函数模板接受所有兼容类型

        之前使用的智能指针时“行为像指针”的对象,不用担心内存泄漏、重复析构等问题,但真实指针做得更好的一件事情是,支持隐式转化

class Top{};
class Middle:public Top{};
class Bottom:public Middle{};
Top *pt1=new Middle;        //将Middle*转换为Top*
Top *pt2=new Bottom;        //将Bottom*转换为Top*
const Top *pct2=pt1;        //将Top*转换为const Top*

        如果我们想在自定义的智能指针中模拟上述转换,我们希望以下代码通过编译:

tenplate<typename T> class SmartPtr{
public:
explicit SmartPtr(T *realPtr);            //构造函数
...
};
SmartPtr<Top> pt1=SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2=SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2=pt1;

        但同一个template的不同具现体之间并不存在任何联系。所以编译器认为Smart<Middle>和Smart<Top>为完全不同的class。我们可以编写构造函数完成不同类型之间的转换,但如果在继承体系中又有新的类型加入,我们就需要添加大量的代码,以完成转换,因此这并不是一个可实行的方法。

        因此我们应该编写一个构造模板。这样的模板是所谓的member function templates(简称member templates):

template<typename T>class SmartPtr{
public:
template<typename U>SmartPtr(const SmartPtr<U> &other);
...
};

        上述代码是说:任何T和U,可以根据SmartPtr<U>生成SmartPtr<T>,我们称这个函数为泛化copy构造函数。但此函数提供的东西比我们需要的更多,我们希望根据SmartPtr<Bottom>生成SmartPtr<Top>,却不希望根据SmartPtr<Top>生成SmartPtr<Bottom>,因此我们必须对所创建的成员进行筛除

        假设SmartPtr提供一个get成员函数,返回指针对象所持有的原始指针的副本,我们可以在构造函数模板实现代码中约束转换行为:

template<typename T>class SmartPtr{
public:
template<typename U>SmartPtr(const SmartPtr<U> &other):heldPtr(other.get()){...}
T* get() const{return heldPtr;}
...
private:
T *heldPtr;
};

        使用类型为U*的指针初始化类型为T*的成员变量。这个行为只有当“存在某个隐式转换可以将某个U*指针转换为T*指针”时才能通过编译。

        编译器会为我们产生成员函数:copy构造函数和copy assignment操作符。一旦类型T和U相同,泛化copy构造函数就会具现化为copy构造函数,那么编译器会为我们生成copy构造函数吗?

        在class内部声明泛化copy构造函数并不会阻止编译器生成它们的copy构造函数。对于copy assignment操作符同样如此。

请记住:

        请使用member function template(成员函数模板)生成“可接受所有兼容类型”的函数

        如果你的声明member templates用于泛化copy构造或泛化assignment操作,你还是需要声明正常的copy构造函数和copy assignment操作符


条款46:需要类型转换时请为模板定义非成员函数

        条款24讨论过所有实参需要隐式转换时,需要为non_member,此处我们将Rational和operator*模板化:

template<typename T>class Rational{
public:
Rational(const T &numerator=0,const T &denomirator=1);
const T numerator()const;
const T denominator()const;
...
};
template<typename T>const Rational<T> operator*(const Rational<T> &lhs,const Rational<T> &rhs);
Ratioanl<int> resualt=oneHalf*2;        //错误,无法通过编译

        调用函数模板的时候需要进行实参推断。operator*的第一个实参是Rational<T>,而oneHalf为Rational<int>,因此很容易推断出T为int型,但第二个实参被声明为Rational<int>,但实际类型是int。编译器如何推断T的类型?或许会期望将2通过构造函数转换为Rational<int>,但template实参推导过程中不会将隐式类型转化函数纳入考虑

        template class内的friend声明式可以涉指某个特定函数。这意味着class Rational<T>可以声明operator*是它的一个friend函数。template class并不依赖实参推导,所以编译器总能在class Rational<T>具现化时得知T:

template<typename T>class Rational{
public:
...
friend const Rational operator*(const Rational &lhs,const Rational &rhs);
};
template<typename T>const Rational<T> operator*(const Ratioanl<T> &lhs,const Ratioanl<T> &rhs)
{...}

        现在operator*的混合式调用就可通过编译了,因为此时operator*不再为函数模板而是一个函数,因此可在调用时使用隐式转换函数。

        如果将friend删除,即将operator*函数定义为Rational class类的成员函数,编译器又不能通过编译,因为类模板的成员函数就是函数模板(但有区别,不进行模板实参推断,而是由调用该函数的对象的类型确定,因此可以进行常规转换),参数匹配时不发生隐式转化

        上述办法虽然能解决编译问题,但却无法连接。因为虽然编译器知道要调用哪个函数,但那个函数被声明于Rational内,并没有定义,我们想在class外部定义,但是行不通——如果我们声明了一个函数,就有责任定义它,需要将定义式合并至声明式内

        我们虽然将函数声明为friend,但却与传统的friend用途“访问class的non_public成分”毫不相干。为了让类型转化施加于所有形参身上,就需要一个non_member函数;为了令这个函数被自动具现化,我们需要将它声明在class内部;而class内部声明non_member函数的唯一办法是:令它成为一个friend。

请记住:

        当我们编写一个class template,而它所提供的“与此templates相关的”函数支持“所有参数的隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”


条款47:请使用traits classes表现类型信息

        STL中有一个advance,用来将某个迭代器移动指定距离:

template<typename IterT,typename DistT>void advance (IterT &iter,DistT &d);

        观念上advance只做iter+=d,但实际上只有random access迭代器才支持+=,而其他的只能重复++或者--操作。

        STL有五种迭代器:

  • input迭代器,只能向前移动,一次一步,只能读取(不可涂写)所指,并且只能读取一次
  • output迭代器,只能向前移动,一次一步,只能涂写(不可读取)所指,并且只能涂写一次
  • forward迭代器,可做上述两种迭代器的工作,可读或写一次以上
  • bidirectional相较于forward迭代器,可以向后移动
  • random access迭代器可做上述迭代器的工作,并且可以在常量时间内向前或向后跳跃任意距离,类似于内置指针

        对上述五种迭代器,C++标准库提供专属的标卷结构(tag struct)加以确认:

struct input_iterator_tag{};
struct output_itertator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
struct bidirectional_iterator_tag:public:forward_iterator_tag{};
struct random_access_iterator_tag:public bidirectional_iterator_tag{};

        我们希望以一下代码实现advance:

template<typename IterT,typename DistT>void advance(IterT &iter,DistT &d)
{
if(iter is a random access iterator)
    iter+=d;
else
{
if(d>0){while(d--) ++iter;}
else{while(d++) --iter;}
}
}

        我们需要判断iter是否为random access迭代器,那就是traits所做的事情:它们允许你在编译期取得某些类型信息。traits并不是C++关键字或预先定义好的构件,而是一种技术,也是协议。它要求对内置类型和用户自定义类型的表现必须一样好,即traits技术必须也能施于内置类型身上

        标准做法是将它放入一个template及其一个或多个特化版本中,针对迭代器的对象被命名为iterator_traits:

template<typename IterT>struct iterator_traits;        //用来处理迭代器相关分类信息

        在struct iterator_traits<IterT>内声明某个typedef名为iterator_category,用来确认IterT的迭代器分类,iterator_traits对于用户自定义迭代器类型必须嵌套一个typedef名为iterator_category:

template<...>class deque{            //deque迭代器可随机访问,此处省略参数
public:
class iterator{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
template<...>class list{            //list迭代器可双向访问,此处省略参数
public:
class iterator{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};

        应用于iteraotr_traits,就响应iterator class的嵌套式typedef:

template<typename IterT>struct iterator_traits{
typedef typename IterT::iterator_category iterator_category;        //针对对不同类型的IterT,typedef其所定义的iterator_category
...
};

        对于指针迭代器,提供一个偏特化版本:

template<typename IterT>struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag iterator_category;
...
};

        有了上述定义,我们可以实现之前的伪代码:

template<typename IterT,typename DistT>void advance(IterT &iter,DistT &d)
{
if(typeid(typename std::iterator_traits<IterT>::iterator_actegory)==typeid(std::random_access_iterator_tag))
...
}

        上述代码中,if条件中的语句可在编译器确定,但是if语句却是在运行期核定,这会导致文件膨胀,因此我们采用重载方法,增加一个形参,用来决定调用哪一个重载函数:

template<typename IterT,typename DistT>void doAdvance(IterT &iter,DisT &d,std::random_access_iterator_tag)
{
iter+=d;
}
template<typename IterT,typename DistT>void doAdvance(IterT &iter,DisT &d,std::bidirectional_iterator_tag)
{
if(d>0){while(d--)++iter;}
else{while(d++)--iter;}
}
template<typename IterT,typename DistT>void doAdvance(IterT &iter,DisT &d,std::input_iterator_tag)
{
if(d<0){throw std::out_of_range("Negative distance");}
while(d--)++iter;
}
template<typename IterT,typename DistT>void advance(IterT &iter,DistT &d)
{
doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
}
请记住:

        Traits class使得“类型相关信息”在编译期可用。他它们以templates和“templates 特化”完成实现

        整合重载技术后,traits class有可能在编译期对类型执行if...else测试


条款48:认识template元编程(此篇仅限于了解模板元编程)

        template metaprogramming(TMP)执行于编译期,可将工作从运行期转移到编译期。

        用TMP编写计算阶乘:

template<uunsigned n>struct Factorial{
enum{value=n*Factory<n-1>::value};
};
template<>struct Factory<0>{
enum={value=1};
};

        每个Factory template具现体都是一个struct,每个struct都使用enum hack声明一个名为value的TMP变量,value用来保存当前阶乘值。

请记住:

        template metaprogramming可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率

        TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值