Item 41:了解隐式接口和编译期多态
- ① “classes 和 templates 都支持接口(interfaces)和多态(polymorphism)。”
- 类中的接口和多态
- 接口:
可以通过定义一个包含纯虚函数的抽象基类(即接口类)来表示一个接口。派生类可以继承这个基类并实现其纯虚函数,这样不同的派生类可以实现不同的行为。
- 多态:
通过动态多态实现,即使用基类指针或引用调用派生类的重写方法,这种多态在运行时通过虚表(vtable)实现。
- 接口:
- 模板中的接口和多态
- 接口:
模板没有传统的接口概念,但可以通过约定的“接口”来要求模板参数具备某些行为或方法。
- 多态:
模板实现的多态性是静态多态(编译时多态)。在编译时,模板会根据具体的类型参数生成不同的代码版本,因此不需要虚表。每个不同的类型都会实例化出不同的函数版本,从而避免运行时开销。
- 接口:
- 类多态和模板多态对比
特性 类(动态多态) 模板(静态多态) 多态类型 运行时多态 编译时多态 开销 依赖虚函数表,略有运行时开销 编译时展开,无额外运行时开销 接口要求 通过继承抽象基类,实现特定接口 通过约定,要求类型符合接口 灵活性 适用于多种类型的指针或引用 适用于编译期已知的特定类型 适用场景 适合运行时接口一致的多态性 适合需要最大性能的泛型编程
- 类中的接口和多态
- ② “对 classes 而言,接口是显式的(explicit),以函数签名为中心。多态则是通过 virtual 函数发生于运行期。”
- ③ “对 template 参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期。”
接口的隐式性:
模板的接口并不是显式地通过函数签名来定义的,而是基于模板参数支持的有效表达式。模板代码不会检查模板参数的类型是否具有特定的函数,而是依赖编译器在实例化模板时检测传入的类型是否具备所需的功能。这种依赖被称为鸭子类型(duck typing)——即只要类型“看起来像”具有所需接口,就可以使用。
template <typename T> void renderShape(const T& shape) { shape.draw(); // 假设 T 类型有 draw() 方法 }
在这个例子中,
renderShape
模板并未显式要求T
具有draw()
方法。只有在实例化renderShape
时,编译器才会检查传入的类型是否支持draw()
,因此模板的接口是隐式的。
Item 42:了解 typename 的双重含义
- ① “声明 template 参数时,前缀关键字 classes 和 typename 可互换。”
- ② “请使用关键字 typename 标识嵌套从属类型名称;但不得在 base class lists(基类列)或 member initialization list 内以它作为 base class 修饰符。”
1. 使用
typename
标识嵌套从属类型在模板中,当访问某个依赖于模板参数的嵌套类型时,编译器无法提前知道这个嵌套名称到底是类型还是成员变量,需要使用
typename
来明确声明它是一个类型。例如:template <typename T> class Container { public: typename T::value_type element; // 明确 T::value_type 是一个类型 };
在上面的代码中,
T::value_type
是依赖于T
的嵌套类型,使用typename
告诉编译器这是一个类型而不是变量。2. 不得在基类列表或成员初始化列表中使用
typename
在以下情况中,不得使用
typename
关键字作为类型修饰符:- 基类列表:在基类列表中指定基类时,即使基类依赖于模板参数,也不需要且不允许使用
typename
:
template <typename T> class Derived : public T::BaseType { // 不需要 typename public: // ... };
- 成员初始化列表:在构造函数的成员初始化列表中,使用依赖类型的基类构造函数初始化时,也不能使用
typename
:
template <typename T> class Derived : public T::BaseType { public: Derived() : T::BaseType() { // 不允许使用 typename // ... } };
- 基类列表:在基类列表中指定基类时,即使基类依赖于模板参数,也不需要且不允许使用
Item 43:学习处理模板化基类内的名称
- “可在 derived class templates 内通过
this->
指涉 base class templates 内的成员名称,或藉由一个明白写出的 ‘base class 资格修饰符’ 完成。”观察下面的例子:
class CompanyA { public: ... void sendCleattext(const std::string& msg); void sendEncrypted(const std::string& msg); ... }; class CompanyB { public: ... void sendCleattext(const std::string& msg); void sendEncrypted(const std::string& msg); ... }; ... // 针对其他公司设计的 classes class MsgInfo {...}; // 这个classes用来保存信息,以备将来产生信息 template<typename Company> class MsgSender { public: ... void sendClear(const MsgInfo& info) { std::string msg; // 根据 info 产生 信息 ... Company c; c.sendCleartext(msg); } void sendSecret(const MsgInfo& info) { std::string msg; // 根据 info 产生 信息 ... Company c; c.sendEncrypted(msg); } }; // 当想要在每次送出信息时志记某些信息。 // derived class 可轻易加上这样的功能 template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { // 将 传送前 的信息写入 log ... sendClear(info); // 调用 base class 函数;这段代码无法通过编译 // 将 传送后 的信息写入 log ... } };
正如注释中所说,
sendClear(info);
无法通过编译,原因看下面的例子:class CompanyZ { public: ... // 这个类不提供 sendCleartext 函数 void sendEncrypted(const std::string& msg); ... };
对于 CompanyZ 来说,一般性的 MsgSneder template 并不合适,因为 template 提供了一个 sendClear 函数,而这个函数调用了 CompanyZ 中并没有的 sendCleartext 函数。欲解决这个问题,可以针对 CompanyZ 产生一个 MsgSender 特化版。
// 一个全特化的 MsgSender,它和一般 template 相同,差别只在于它删掉了 sendClear template<> class MsgSender<CompanyZ> { public: ... void sendSecret(const MsgInfo& info) { std::string msg; // 根据 info 产生 信息 ... Company c; c.sendEncrypted(msg); } };
所以
sendClear(info);
无法通过编译的原因是:它知道 base class templates 有可能被特化,而那个特化版本可能不提供和一般性 template 相同的接口。因此它往往拒绝在 templatized base classes(模板化基类,本例中的MagSender<Company>
)内寻找继承而来的名称。有三个办法令C++ “不进入 templatized base classes 观观察” 的行为失效:
(1)在 base class 函数调用动作之前加上
this->
template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { // 将 传送前 的信息写入 log ... this->sendClear(info); // 假设 sendClear 将被继承 // 将 传送后 的信息写入 log ... } };
(2)使用 using 声明式
template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: // 告诉编译器,请它假设 sendClear 位于 base class 内 using MsgSender<Company>::sendClear; ... void sendClearMsg(const MsgInfo& info) { // 将 传送前 的信息写入 log ... sendClear(info); // 将 传送后 的信息写入 log ... } };
(3)指出被调用的函数位于 base class 内
template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { // 将 传送前 的信息写入 log ... MsgSender<Company>::sendClear(info); // 将 传送后 的信息写入 log ... } };
注意:这种方法往往是最不让人满意的一个解法,因为如果被调用的是 virtual 函数,上述的明确资格修饰(explicit qualification) 会关闭 “virtual 绑定行为”。
Item 44:将与参数无关的代码抽离 templates
- ① “Templates 生成多个 classes 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。”
- ② “因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 template 参数。”
- 模板版本(如
Buffer<10>
,Buffer<100>
等):每个不同的模板参数Size
都会生成独立的类定义。对于Buffer<10>
和Buffer<100>
,编译器会分别生成两个独立的类代码,包括构造函数、clear
函数以及每个类的data
数组大小。这种编译时的多重实例化增加了代码体积(即编译器膨胀问题)。 - 非模板版本(以
size
参数代替模板):不论size
值是多少,只生成一个Buffer
类定义,并且所有实例共享同一个类代码。虽然会在运行时生成多个对象,但代码段保持不变,不会因为对象大小不同而重新生成类代码。
template <size_t Size> class Buffer { public: void clear() { for (size_t i = 0; i < Size; ++i) { data[i] = 0; } } private: int data[Size]; };
如果这个模板用不同的
Size
值实例化,编译器将为每个Size
值生成一个不同的Buffer
类。这样会导致多个Buffer
类实例被生成,从而造成代码膨胀。替换为类成员变量
可以将
Size
从模板参数中移除,并用一个构造函数参数或类成员变量代替:class Buffer { public: Buffer(size_t size) : size(size), data(new int[size]) {} void clear() { for (size_t i = 0; i < size; ++i) { data[i] = 0; } } private: size_t size; std::unique_ptr<int[]> data; };
替换为函数参数
对于某些使用场景,也可以直接在成员函数中引入该参数,而不是在类中保存:
class Buffer { public: void clear(size_t size) { for (size_t i = 0; i < size; ++i) { data[i] = 0; } } private: int* data = new int[default_size]; };
- 模板版本(如
- ③ “因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述(binary representation)的具现类型(instantiation types)共享实现码。”
template <typename T> class Container { public: void doSomething() { /* ... */ } };
对于
Container<int>
和Container<long>
,编译器会生成两份独立的代码,即使int
和long
的二进制结构可能一致。这种机制导致了代码膨胀。template <typename T> class Container { public: void doSomething() { /* 默认实现 */ } }; // 特化:使 Container<int> 和 Container<long> 使用相同代码 template <> class Container<long> : public Container<int> {};
在这个例子中,
Container<long>
特化直接复用了Container<int>
的代码,这样编译器只生成Container<int>
的实现代码。
Item 45:运用成员函数模板接受所有兼容类型
- ① “请使用 member function templates(成员函数模板)生成 ‘可接受所有兼容类型’ 的函数。”
- ② “如果你声明 member templates 用于 ‘泛化 copy 构造’ 或 ‘泛化 assignment 操作’ ,你还是需要声明正常的 copy 构造函数和 copy assignment 操作符。”
当有泛化版本存在时,编译器不会自动生成默认的拷贝构造函数和拷贝赋值操作符。
假设我们有一个类
Widget
,想通过成员模板实现泛化的拷贝构造和赋值操作:#include <iostream> class Widget { public: int data; // 泛化拷贝构造函数模板 template <typename T> Widget(const T& rhs) : data(rhs.data) {} // 泛化赋值操作符模板 template <typename T> Widget& operator=(const T& rhs) { data = rhs.data; return *this; } };
在这个示例中:
- 泛化拷贝构造函数模板和赋值操作模板允许
Widget
从不同类型的对象(T
)构造和赋值。 - 然而,这些模板不会处理相同类型的
Widget
对象拷贝,即Widget w1 = w2;
这种普通的拷贝操作,编译器不会自动生成缺失的拷贝构造函数和赋值操作。
因此,如果想支持
Widget
的正常拷贝操作,仍然需要手动添加:class Widget { public: int data; // 普通的拷贝构造函数 Widget(const Widget& rhs) : data(rhs.data) {} // 普通的拷贝赋值操作符 Widget& operator=(const Widget& rhs) { if (this != &rhs) { data = rhs.data; } return *this; } // 泛化拷贝构造函数模 template <typename T> Widget(const T& rhs) : data(rhs.data) {} // 泛化赋值操作符模板 template <typename T> Widget& operator=(const T& rhs) { data = rhs.data; return *this; } };
- 泛化拷贝构造函数模板和赋值操作模板允许
Item 46:需要类型转换时请为模板定义非成员函数
- “当我们编写一个 class template,而它所提供之 ‘与此 template 相关的’ 函数支持 ‘所有参数之隐式类型转换’ 时,请将那些函数定义为 ‘class template’ 内部的 friend 函数。”
观察下面的例子:
template<typename T> class Rational { public: Rational(const T& numerator = 0, const T& denominator = 1); const T numerator() const; const T denominator() const; }; template<typename T> const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs) { } Rational<int> oneHalf(1, 2); Rational<int> result = oneHalf * 2; // 编译错误
这个例子中,
Rational<int> result = oneHalf * 2;
无法通过编译,原因是oneHalf * 2
中的2
是一个int
类型的值,而operator*
只接受两个Rational<T>
类型的参数。由于operator*
是一个函数模板,编译器不会自动将int
转换为Rational<int>
,导致类型不匹配的错误。你也许会期盼编译器使用
Rational<int>
的 non-explicit 构造函数将2转化为Rational<int>
,进而将 T 推导为 int,但它们不那么做,因为在 template 实参推导过程中从不将隐式类型转换函数纳入考虑。解决办法: 将 operator* 定义为 Rational 的友元。
template<typename T> class Rational { public: ... friend const Rational operator* (const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } ... }
当我们将
operator*
定义为Rational
的友元时,编译器会把operator*
的定义视为Rational
类的“成员”——尽管它不是真正的成员函数。这意味着operator*
会自动绑定到Rational
的模板参数T
,所以在Rational<T>
作用域内调用时,T
的类型自动被传递给operator*
。
Item 47:请使用 traits classes 表现类型信息
- ① “Traits classes 使得 ‘类型相关信息’ 在编译期可用。它们以 templates 和 ‘templates 特化’ 完成实现。”
- ② “整合重载技术(overloading)后,traits classes 有可能在编译期对类型执行 if...else 测试。”
Item 48:认识模板元编程
- ① “Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。”
- ② “TMP 可被用来生成 ‘基于政策选择组合’ (based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。”
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/cjjbc/639.html