【C++】《Effective C++》第七章
第七章 模板与泛型编程
条款41:了解隐式接口和编译期多态
面向对象设计中的类(class
)考虑的是显式接口(explict interface
)和运行时多态,而模板编程中的模板(template
)考虑的是隐式接口(implict interface
)和编译器多态。
请记住
classes
和templates
都支持接口(interfaces
)和多态(polymorphism
)。- 对
classes
而言,接口是显式的(explict
),以函数签名为中心。多态则是通过virtual
函数发生于运行期。 - 对
template
而言,接口是隐式的(implict
),基于有效表达式。多态则是通过template
具现化和函数重载解析(function overloadding resolution
)发生于编译器。
条款42:了解typename
的双重意义
在template
声明式中,class
和typename
有什么不同?
template<class T> class Widget;
template<typename T> class Widget;
答案是没有什么不同,当我们声明template
类型参数,class
和typename
的意义完全相同。
然而C++
并不总是把class
和typename
视为等价,有时一定得使用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
参数时,前缀关键字class
和typename
可互换。 - 请使用关键字
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 LoggingMsgSender
的sendClearMsg
就会调用不存在的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();
}
};
- 对于类型参数而造成的代码膨胀,可以让不同实例化的模板类共用同样的二进制表示:
int
和long
在多数平台都是一样的底层实现,然而模板却会实例化分为两份,因为它们类型不同。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
的两个效力:
- 它让某些事情更容易。如果没有它,有些事情将是困难的,甚至是不可能的。
- 执行于编译期,因此可将工作从运行期转移到编译期,但是会导致以下结果:
- 某些原本在运行期才能侦测到的错误现在可以在编译期中找出来。
- 使用
TMP
的C++
程序可能在每一个方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。 - 编译时间变得更长了。
#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++》第七章的更多相关文章
- C++Primer 第七章
//1.定义在类内部的函数是隐式内联的. //2.默认情况下,this指针的类型是指向类类型非常量版本的常量指针.对于类的常量成员函数的声明方法是:将const放置于成员函数的参数列表后,用于修饰th ...
- C primer plus 读书笔记第六章和第七章
这两章的标题是C控制语句:循环以及C控制语句:分支和跳转.之所以一起讲,是因为这两章内容都是讲控制语句. 第六章的第一段示例代码 /* summing.c --对用户输入的整数求和 */ #inclu ...
- C++ Primer Plus学习:第七章
C++入门第七章:函数-C++的编程模块 函数的基本知识 要使用C++函数,必须完成如下工作: 提供函数定义 提供函数原型 调用函数 库函数是已经定义和编译好的函数,可使用标准库头文件提供原型. 定义 ...
- 【C++】《C++ Primer 》第七章
第七章 类 一.定义抽象数据类型 类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation). 数据抽象是一种依赖于接口(interface)和实现(imple ...
- 精通Web Analytics 2.0 (9) 第七章:失败更快:爆发测试与实验的能量
精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第七章:失败更快:爆发测试与实验的能量 欢迎来到实验和测试这个棒极了的世界! 如果Web拥有一个超越所有其他渠道的巨大优势,它就 ...
- 《Entity Framework 6 Recipes》中文翻译系列 (38) ------ 第七章 使用对象服务之动态创建连接字符串和从数据库读取模型
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第七章 使用对象服务 本章篇幅适中,对真实应用中的常见问题提供了切实可行的解决方案. ...
- 《Entity Framework 6 Recipes》中文翻译系列 (41) ------ 第七章 使用对象服务之标识关系中使用依赖实体与异步查询保存
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 7-7 标识关系中使用依赖实体 问题 你想在标识关系中插入,更新和删除一个依赖实体 ...
- Java语言程序设计(基础篇) 第七章 一维数组
第七章 一维数组 7.2 数组的基础知识 1.一旦数组被创建,它的大小是固定的.使用一个数组引用变量,通过下标来访问数组中的元素. 2.数组是用来存储数据的集合,但是,通常我们会发现把数组看作一个存储 ...
- objective-c第七章课后练习2
题:改变第七章例子中print方法,增加bool参数,判断如果是YES则对分数进行约简 @interface Fraction : NSObject { //int num,den; } @prope ...
- 读《编写可维护的JavaScript》第七章总结
第七章 事件处理 7.1 典型用法 作者首先给了个我们一个处理事件的方法.看起来也没啥俩样,不过后来给出的优化方法很值得学习: // 不好的写法 function handleClick(even ...
随机推荐
- vue通过事件向父级组件发送消息(官网点击放大例子)
注意:Vue.component一定要写在new Vue之前 在页面中使用组件 整体代码示例
- jvm 模型
- 题解-洛谷P5217 贫穷
洛谷P5217 贫穷 给定长度为 \(n\) 的初始文本 \(s\),有 \(m\) 个如下操作: \(\texttt{I x c}\),在第 \(x\) 个字母后面插入一个 \(c\). \(\te ...
- ActiveMq PUT任意文件上传漏洞(CVE-2016-3088)漏洞复现
漏洞原理 该漏洞出现在fileserver应用中,ActiveMQ中的fileserver服务允许用户通过HTTP PUT方法上传文件到指定目录.Fileserver支持写入文件(不解析jsp),但是 ...
- Centos7.6上利用docker搭建Jenkins来自动化部署Django项目
一般情况下,将一个项目部署到生产环境的流程如下: 需求分析-原型设计-开发代码-内网部署-提交测试-确认上线-备份数据-外网更新-最终测试,如果发现外网部署的代码有异常,需要及时回滚. 整个过程相当复 ...
- 用python写图片格式批量处理工具
一.思路分析 其实,照片处理要求很简单,主要是两个方面:一个是调整图片尺寸(即宽x高),另一个是调整图片的大小(即压缩).为了实现这两个功能,利用python中的PIL库即可,其安装方法如下: pip ...
- 基于frp的内网穿透实例4-为本地的web服务实现HTTPS访问
原文地址:https://wuter.cn/1932.html/ 一.想要实现的功能 目前已经实现将本地的web服务暴露到公网,现想要实现https访问.(前提:已经有相应的证书文件,如果没有就去申请 ...
- 进入mysql数据库修改密码
mysql -hlocalhost -uroot -p #修改密码mysql> set password for root@localhost = password('root');#启动数据库 ...
- ubuntu系统64位dnw
/* dnw2 linux main file. This depends on libusb. * * * * Author: Fox <hulifox008@163.com> * * ...
- 如何在K8S中优雅的使用私有镜像库 (Docker版)
前言 在企业落地 K8S 的过程中,私有镜像库 (专用镜像库) 必不可少,特别是在 Docker Hub 开始对免费用户限流之后, 越发的体现了搭建私有镜像库的重要性. 私有镜像库不但可以加速镜像的拉 ...