第七章 模板与泛型编程

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

面向对象设计中的类(class)考虑的是显式接口(explict interface)和运行时多态,而模板编程中的模板(template)考虑的是隐式接口(implict interface)和编译器多态。

请记住

  • classestemplates都支持接口(interfaces)和多态(polymorphism)。
  • classes而言,接口是显式的(explict),以函数签名为中心。多态则是通过virtual函数发生于运行期。
  • template而言,接口是隐式的(implict),基于有效表达式。多态则是通过template具现化和函数重载解析(function overloadding resolution)发生于编译器。

条款42:了解typename的双重意义

template声明式中,classtypename有什么不同?

template<class T> class Widget;
template<typename T> class Widget;

答案是没有什么不同,当我们声明template类型参数,classtypename的意义完全相同。

然而C++并不总是把classtypename视为等价,有时一定得使用typename

#include <iostream>
#include <vector> template<typename C>
void printTwoValue(const C& container) { // 打印容器内的第二个元素
if(container.size() >= 2) {
C::const_iterator iter(container.begin()); // Error
// typename C::const_iterator iter(container.begin()); // Success
// auto iter(container.begin()); // Success
++iter;
int value = *iter;
std::cout << value << std::endl;
}
} int main(int argc, char* argv[]) { std::vector<int> vec{1, 2, 3, 4}; printTwoValue(vec); return 0;
}
#include <iostream>
#include <vector> template<typename C>
void printTwoValue(const C& container) { // 打印容器内的第二个元素
if(container.size() >= 2) {
// C::const_iterator iter(container.begin()); // Error
// typename C::const_iterator iter(container.begin()); // Success
auto iter(container.begin()); // Success
++iter;
int value = *iter;
std::cout << value << std::endl;
}
} int main(int argc, char* argv[]) { std::vector<int> vec{1, 2, 3, 4}; printTwoValue(vec); return 0;
}

一般情况下,任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。但是typename只被用来验证嵌套从属类型名称。其他名称不该有它存在。

templare<typename C>
void f(const C& container, typename C::iterator iter);

但是这一个规则有一个例外typename不能出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初始值列表)中作为base class修饰符。

tempalate<typename T>
class Derived: public Base<T>::Nested { // base classes list中不允许"typename"
public:
explicit Derived(int x): Base<T>::Nested(x) { //
typename Base<T>::Nested temp;
//
} };

请记住

  • 声明template参数时,前缀关键字classtypename可互换。
  • 请使用关键字typename标识嵌套从属类型名称,但是不可以在base class list(基类列)或member initialization list(成员初始值列表)内以它们作为base class修饰符。
  • 最重要的,当你不确定是否应该使用typename时,使用C++11新标准auto关键字最为稳妥。

条款43:学习处理模板化基类内的名称

假设以下MsgSender类可以通过两种方式发送信息到各个公司:

template<typename Company>
class MsgSender {
public:
// ...
// 1. 发送原始文本
void sendClear(){
Company c;
c.sendClearText();
} // 2. 发送加密后的文本
void sendSecret(){
// ...
}
};

假设我们有时候想要每次发出信息时记录某些信息,因此有了如下派生类:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
sendClear(); // Error,因为不知道继承什么样的类,Company是个template参数
// 将传送后的信息写入日志
}
// ...
};

现在的问题是,如果有一个公司CompanyZ支持加密传送,那么泛化的MsgSender就不合适了,所以需要产生一个特例化版的MsgSender

template<>
class MsgSender<CompanyZ> {
public: // 只支持发送加密后的文本
void sendSecret(){
// ...
}
};

base class被指定为MsgSender时,其内不包含sendClear方法,那么dervied class LoggingMsgSendersendClearMsg就会调用不存在的sendClear方法。

正是因为知道base class templates有可能被特例化,而且特例化版本不提供和一般性template相同的接口,因此C++往往拒绝模板化基类(templatized base classes)

解决这个问题的办法有三个,它们会通知编译器进入base class作用域查找继承而来的名称。

  • 方法1:使用this->
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
this->sendClear(); // Success,假设sendClear被继承
// 将传送后的信息写入日志
}
// ...
};
  • 方法2:使用using
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear(); // 告诉编译器,让她假设sendClear位于base class内
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
sendClear(); // Success,假设sendClear被继承
// 将传送后的信息写入日志
}
// ...
};
  • 方法3:通过作用域明确指出
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// ...
void sendClearMsg() {
// 将传送前的信息写入日志
MsgSender<Company>::sendClear(); // Success,假设sendClear被继承
// 将传送后的信息写入日志
}
// ...
};

方法3有一个问题存在,假如被调用的是virtual函数,这样会关闭"virtual函数的绑定行为"。

请记住

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

条款44:将与参数无关的代码抽离templates

模板提供的是编译期的多态,即使你的代码看起来完全简短,生成的二进制文件也可能包含大量冗余代码。把模板中参数无关的代码重构到模板外便可以有效地控制模板产生的代码膨胀。

  • 对于非类型模板参数而造成的代码膨胀,用函数参数或成员变量来替换模板参数即可消除冗余:
// 非类型模板参数而造成的代码膨胀
template<typename T, std::size_t n>
class SquareMatrix {
public:
// ...
void invert(); // 求逆矩阵
};
SquareMatrix<double, 5> s1;
SquareMatrix<double, 10> s2;
s1.invert();
s1.invert(); // 使用函数参数消除重复
template<typename T>
class SquareMatrixBase {
protected:
// ...
void invert(); // 求逆矩阵
}; template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase {
private:
using SquareMatrixBase<T>::invert();
public:
// ...
void invert() {
this->invert();
}
};
  • 对于类型参数而造成的代码膨胀,可以让不同实例化的模板类共用同样的二进制表示:

    • intlong在多数平台都是一样的底层实现,然而模板却会实例化分为两份,因为它们类型不同。
    • List<int>List<const int>List<double*>的底层实现也是一样的,但是因为指针类型不同,也会实例化为多份模板类,如果某些成员函数操作强型指针(untyped pointers,即void*)的函数,应该令它们调用另一个操作无类型指针(void*)的函数,后完成实际工作。

请记住

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
  • 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
  • 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。

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

假设SmartPtr是一种智能指针,并且它是一个template class,现在有一个继承体系:

class Top{};
class Middle: public Top {};
class Bottom: public Top {};

现在希望通过一个SmartPtr<Bottom>SmartPtr<Middle>来初始化一个SmartPtr<Top>,如果是指针,即Middle*Bottom*可以隐式转换为Top*,问题是:同一个template的不同具现体之间不存在什么与生俱来的固有关系,即使具现体之间具有继承关系。所以,SmartPtr<Bottom>SmartPtr<Middle>并不能隐式转换为SmartPtr<Top>

可以用一个构造函数模板来实现这种转换:

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

请记住

  • 请使用member function templates(成员函数模板)生成"可接受所有兼容类型"的函数。
  • 如果你声明member templates用于"泛化copy构造"或"泛化assignment操作",你还是需要声明正常的copy构造函数和copy assignment操作符。

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

有如下实现:

template<typename T>
class Rational{
public:
Rational(const T&numberator=0, const T&denominator=1);
const T numberator() const;
const T denominator() const;
// ...
}; template<typename>
const Rational<T> operator*(const Rational<T>&lhs, const Rational<T>&rhs){
// ...
} // 使用
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 编译错误

分析:将oneHalf传递给operator*时,它将T推断为int,因此期待第二个参数也为Rational,但是第二个参数为int,由于,C++template实参推导过程中从不将隐式类型转换函数纳入考虑,所以编译错误。

请记住

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

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

traits并不是C++关键字或一个预先定义好的构件,它们是一种技术,也是一个C++程序员共同遵守的协议。

traits,又被叫做特性萃取技术,说得简单点就是提取“被传进的对象”对应的返回类型,让同一个接口实现对应的功能。因为STL的算法和容器是分离的,两者通过迭代器链接。算法的实现并不知道自己被传进来什么。萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。

请记住

  • Traits classes使得"类型相关信息"在编译期可用。它们以templates和"templates特化"完成实现。
  • 整合重载技术(overloading)后,traits classes有可能在编译器对类型执行if...else测试。

条款48:认识template元编程

  • Template mateprogramming(TMP)是编写template-based C++程序并执行于编译期的过程。
  • Template mateprogram(模板元程序)是以C++写成,执行于C++编译器内的程序。

TMP的两个重要特点

  • 基于template。
  • 编译器执行。

TMP的两个效力

  • 它让某些事情更容易。如果没有它,有些事情将是困难的,甚至是不可能的。
  • 执行于编译期,因此可将工作从运行期转移到编译期,但是会导致以下结果:
    • 某些原本在运行期才能侦测到的错误现在可以在编译期中找出来。
    • 使用TMPC++程序可能在每一个方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。
    • 编译时间变得更长了。
#include <iostream>

template <unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
}; template <>
struct Factorial<0> {
enum { value = 1 };
}; int main(int argc, char* argv[]){
std::cout << Factorial<5>::value << std::endl;
std::cout << Factorial<10>::value << std::endl; return 0;
}

请记住

  • TMP(模板元编程)可将工作由运行期转移到编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP可被用来生成"基于政策选择组合"的客户定制代码,也可以用来避免生成对某些特殊类型并不合适的代码。

【C++】《Effective C++》第七章的更多相关文章

  1. C++Primer 第七章

    //1.定义在类内部的函数是隐式内联的. //2.默认情况下,this指针的类型是指向类类型非常量版本的常量指针.对于类的常量成员函数的声明方法是:将const放置于成员函数的参数列表后,用于修饰th ...

  2. C primer plus 读书笔记第六章和第七章

    这两章的标题是C控制语句:循环以及C控制语句:分支和跳转.之所以一起讲,是因为这两章内容都是讲控制语句. 第六章的第一段示例代码 /* summing.c --对用户输入的整数求和 */ #inclu ...

  3. C++ Primer Plus学习:第七章

    C++入门第七章:函数-C++的编程模块 函数的基本知识 要使用C++函数,必须完成如下工作: 提供函数定义 提供函数原型 调用函数 库函数是已经定义和编译好的函数,可使用标准库头文件提供原型. 定义 ...

  4. 【C++】《C++ Primer 》第七章

    第七章 类 一.定义抽象数据类型 类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation). 数据抽象是一种依赖于接口(interface)和实现(imple ...

  5. 精通Web Analytics 2.0 (9) 第七章:失败更快:爆发测试与实验的能量

    精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第七章:失败更快:爆发测试与实验的能量 欢迎来到实验和测试这个棒极了的世界! 如果Web拥有一个超越所有其他渠道的巨大优势,它就 ...

  6. 《Entity Framework 6 Recipes》中文翻译系列 (38) ------ 第七章 使用对象服务之动态创建连接字符串和从数据库读取模型

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第七章 使用对象服务 本章篇幅适中,对真实应用中的常见问题提供了切实可行的解决方案. ...

  7. 《Entity Framework 6 Recipes》中文翻译系列 (41) ------ 第七章 使用对象服务之标识关系中使用依赖实体与异步查询保存

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 7-7  标识关系中使用依赖实体 问题 你想在标识关系中插入,更新和删除一个依赖实体 ...

  8. Java语言程序设计(基础篇) 第七章 一维数组

    第七章 一维数组 7.2 数组的基础知识 1.一旦数组被创建,它的大小是固定的.使用一个数组引用变量,通过下标来访问数组中的元素. 2.数组是用来存储数据的集合,但是,通常我们会发现把数组看作一个存储 ...

  9. objective-c第七章课后练习2

    题:改变第七章例子中print方法,增加bool参数,判断如果是YES则对分数进行约简 @interface Fraction : NSObject { //int num,den; } @prope ...

  10. 读《编写可维护的JavaScript》第七章总结

      第七章 事件处理 7.1 典型用法 作者首先给了个我们一个处理事件的方法.看起来也没啥俩样,不过后来给出的优化方法很值得学习: // 不好的写法 function handleClick(even ...

随机推荐

  1. 【题解】P6329 【模板】点分树 | 震波

    题外话 (其实模板题没必要在这里水题解的--主要是想说这个qwq) 小常数的快乐.jpg 我也不知道我为啥常数特别小跑得飞快--不加快读就能在 luogu 的最优解上跑到 rank5 ( 说不定深夜提 ...

  2. maven私有仓库搭建(nexus)

    搭建是参考博客:https://blog.csdn.net/zn353010922/article/details/79441122 切换到nexus目录的bin下 启动.状态.停止:./nexus ...

  3. TimSort源码详解

    Python的排序算法由Peter Tim提出,因此称为TimSort.它最先被使用于Python语言,后被多种语言作为默认的排序算法.TimSort实际上可以看作是mergeSort+binaryS ...

  4. 微信小程序日期转换、比较、加减

    直接上干货: 在utils目录下新建一个dateUtil.js,代码如下:(在需要用的地方引入这个js,调用相关方法传入对应参数就可以使用了) 该工具脚本,实用性很高,通用于各类前端项目,熟悉后亦可以 ...

  5. 关于C++的异常抛出

    在接触 throw 之前,我们只知道可以通过函数的返回值来获取和定位错误,比如通过 return 来层层返回是一种方法,但如果牵扯到多层函数调用,那么通过 return 来返回错误显得过于拖沓,这时就 ...

  6. js上 五、运算符-1

    5.1.认识运算符 什么是运算符? 运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算. 运算符的应用: 购物车:计算总价,数量: **Js ** 中有哪些运算符? 算术运算符.赋值运算符 ...

  7. 解决Yii ActiveForm监听submit触发2次submit

    今天用yii框架的ActiveForm需要一个奇怪的问题,点击表单提交时会触发两次submit 产生问题的原因: form挂了2次submit事件,一次是yii activeform自带的,一次是我写 ...

  8. day019python之面向对象基础1

    面向对象基础 目录 面向对象基础 1 面向对象基础 1.1 面向对象的由来 1.2 面向对象编程介绍 1.2.1 回顾面向过程设计 1.2.2 面向对象设计 2 类与对象 2.1 基本使用 2.2 示 ...

  9. matplotlib学习日记(十)-划分画布的主要函数

    (1)函数subplot()绘制网格区域中的几何形状相同的子区布局 import matplotlib.pyplot as plt import numpy as np '''函数subplot的介绍 ...

  10. vue3.0自定义指令(drectives)

    在大多数情况下,你都可以操作数据来修改视图,或者反之.但是还是避免不了偶尔要操作原生 DOM,这时候,你就能用到自定义指令. 举个例子,你想让页面的文本框自动聚焦,在没有学习自定义指令的时候,我们可能 ...