第64条 理智地结合静态多态性和动态多态性
1 加 1 可远远不止是 2 :静态多态性和动态多态性是相辅相成的。理解它们的优缺点,善用它们的长处,结合两者以获得两方面的优势。
详细:
1、动态多态性是以某些类的形式出现的,这些类含有虚拟函数和(通过指针或者引用)间接操作的实例。静态多态性则与模板类和模板函数有关。
2、多态性的优势在于,同一段代码能够操作于不同类型,甚至可以是在编写代码时不知道得类型。
3、在 C++ 中动态多态性最擅长于以下几个方面:基于超集/子集关系的统一操作;静态类型检查;动态绑定和分别编译;二进制接口。
4、静态多态性最擅长于以下几个方面:基于语法和语义接口的统一操作;静态类型检查;静态绑定(防止分别编译);效率。
5、应该结合两种多态性,取长补短,同时尽量趋利避害:用静态多态性辅助动态多态性(例如:Command(命令)和 Visitor(访问者)模式);用动态多态性辅助静态多态性(例如:可辨识联合 discriminated union 的实现和 shared_ptr 的 Deleter 参数);任何其他的结合(例如:不要将虚拟函数放入类模板中,除非需要所有虚拟函数每次都实例化(这与模板类型的非虚拟函数简直是大相径庭))。
第65条 有意地进行显式自定义
有意胜过无意,显式强似隐式:在编写模板时,应该有意地、正确地提供自定义点,并清晰地记入文档。在使用模板时,应该了解模板想要你如何进行自定义以将其用于你的类型,并且正确地自定义。
详细:
1、编写模板库时常见的错误是提供无意的自定义点——即在这些点,调用者的代码能够在模板中被查到并使用,但是其实你并不想调用者代码牵涉进来。
2、了解在模板中提供自定义点的三种主要方式,决定在模板的给定点要使用哪一种,并正确地编写出来。然后,检查并确认自己没有无意中在不需要的地方编写了自定义点。
3、第一种方式是常用的“隐式接口”(见 C64)方法,其中模板直接依赖于类型具有给定名字的合适成员这一事实:
// 选择1:通过要求 T 提供 foo 函数的功能
// 作为带给定名字、签名和语义的成员函数来提供自定义点。
template<typename T>
void Sample1(T t)
{
t.foo(); // foo 是一个自定义点
typename T::value_type x; // 另一个例子:提供自定义点来查找类型(通常通过 typedef)
}
4、第二种选择也是使用“隐式接口”方法,但是采用的是一个通过 ADL 查找的非成员函数(即希望它位于模板用来实例化的类型所在的名字空间中),模板现在依赖的是“类型具有给定名字的合适的非成员”这一事实:
// 选择2:通过要求 T 以具有给定名字、签名和语义的非成员函数
// (通常通过 ADL 查找)提供 foo 函数的功能
// 来提供自定义点(这是不会查找类型的唯一选择)。
template<typename T>
void Sample2(T t)
{
foo(t); // foo 是一个自定义点
cout << t; // 另一个例子:带有 operator 符号的 operator<< 是同一种自定义点
}
5、第三种选择是使用特化,这样模板将依赖于“类型已经特化(如果必要)了你提供的另一个类模板”这一事实:
// 选择3:特化 SampleTraits<> 并提供一个(通常是静态的)具有给定
// 名字、签名和语义的函数,从而通过要求 T 提供 foo 函数的功能来
// 提供自定义点。
template<typename T>
void Sample3(T t)
{
S3Traits<T>::foo(t); // S3Traits<>::foo 是一个自定义点
typename S3Traits<T>::value_type x; // 另一个例子:提供查找一个类型(通常通过 typedef)的自定义点
}
6、为了避免无意地提供自定义点,应该:将模板内部使用的任何辅助函数都放入其自己的内嵌名字空间,并用显式的限定调用它们以禁用 ADL:
template<typename T>
void Sample4(T t)
{
S4Helpers::bar(t); // 禁用 ADL:bar 不是自定义点
(bar)(t); // 另一种方式
}
7、要避免依靠依赖名,简而言之,在引用依赖基类的任何成员时,应该总是用基类名或者 this-> 显式地进行限定,可以将此当作强制编译器按你的意图行事的一种奇妙方式:
template<typename T>
class C : X<T>
{
typename X<T>::SomeType s; // 使用基类的内嵌类型或者 typedef
public:
void f()
{
X<T>::baz(); // 调用基类成员函数
this->baz(); // 另一种方式
}
};
第66条 不要特化函数模板
只有在能够正确实施的时候,特化才能起到好作用:在扩展其他人的函数模板(包括 std::swap)时,要避免尝试编写特化代码;相反,要编写函数模板的重载,将其放在重载所用的类型的名字空间中(见 C56, C57)。编写自己的函数模板时,要避免鼓励其他人直接特化函数模板本身(替代方法见 C65)。
详细:
1、重载函数模板是没有问题的。糟糕的是,特化函数模板很不直观:不可能部分地特化函数模板,只能完全特化;函数模板特化决不能参与重载。
2、如果要编写一个函数模板,应该将它编写成一个永远都不会被特化或者被重载的函数模板,并使用一个类模板来实现函数模板。
3、如果正在使用其他人编写的没有使用以上技术的普通旧式函数模板(即没有使用类模板实现的函数模板),而且你想编写自己的应该参与重载的特殊版本,那么请不要将其编写成特化,要将其编写为重载非模板函数(另见 C57, C58)。
第67条 不要无意地编写不通用的代码
依赖抽象而非细节:使用最通用、最抽象的方法来实现一个功能。
详细:
1、编写代码时,尽可能使用最抽象的方法来完成任务。与此相反,毫无道理地依赖细节的代码将是僵化而脆弱的。
2、使用 != 代替 < 对迭代器进行比较
3、使用迭代代替索引访问
4、使用 empty() 代替 size() == 0
5、使用层次结构中最高层的类提供需要的功能
6、编写常量正确的代码(见 C15)
返回 目录