对多态性的理解/要求
要理解多态性——正如计算科学中使用的术语——它有助于从一个简单的测试和定义开始。考虑:
Type1 x;
Type2 y;
f(x);
f(y);
在这里,f()
是执行一些操作并被赋予值x
和y
作为输入。
为了表现出多态性,f()
必须能够对至少两种不同类型(例如int
和double
)的值进行操作,找到并执行不同类型的适当代码。
多态性的 C++ 机制
显式程序员指定的多态性
您可以编写f()
使其可以通过以下任何一种方式对多种类型进行操作:
预处理:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
重载:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
模板:
template <typename T>
void f(T& x) { x += 2; }
虚拟调度:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
其他相关机制
编译器为内置类型、标准转换和强制转换/强制转换提供的多态性将在后面讨论,以确保完整性:
- 无论如何,它们通常被直观地理解(保证“哦,那个”反应),
- 它们影响上述机制的要求门槛和使用的无缝性,以及
- 解释是对更重要概念的一种巧妙分散。
术语
进一步分类
鉴于上述多态机制,我们可以按各种方式对它们进行分类:
何时选择多态类型特定代码?
- 运行时意味着编译器必须为程序在运行时可能处理的所有类型生成代码,并且在运行时选择正确的代码(虚拟调度)
- 编译时意味着在编译期间选择特定类型的代码。这样做的结果是:假设一个程序仅
f
在上面使用int
参数调用 - 取决于使用的多态机制和内联选择,编译器可能会避免为 生成任何代码f(double)
,或者生成的代码可能会在编译或链接的某个时刻被丢弃。(上述所有机制,虚拟调度除外)
支持哪些类型?
1 - 模板非常灵活。 SFINAE(另请参见std::enable_if
)有效地允许参数多态性的几组期望。例如,您可以编码,当您正在处理的数据类型有一个.size()
成员时,您将使用一个函数,否则另一个不需要的函数.size()
(但可能以某种方式受到影响 - 例如使用较慢strlen()
或不打印为有用的消息在日志中)。您还可以在使用特定参数实例化模板时指定临时行为,或者保留一些参数参数(部分模板特化)或不保留(完全特化)。
“多态”
Alf Steinbach 评论说,在 C++ 标准中,多态仅指使用虚拟分派的运行时多态。一般比较 科学。根据 C++ 创建者 Bjarne Stroustrup 的词汇表(http://www.stroustrup.com/glossary.html),含义更具包容性:
多态性——为不同类型的实体提供单一接口。虚函数通过基类提供的接口提供动态(运行时)多态性。重载的函数和模板提供静态(编译时)多态性。TC++PL 12.2.6、13.6.1、D&E 2.9。
这个答案 - 就像问题一样 - 将 C++ 功能与 Comp. 科学。术语。
讨论
C++ 标准使用比 Comp 更窄的“多态性”定义。科学。社区,以确保您的观众相互理解考虑...
- 使用明确的术语(“我们可以让这段代码可重用于其他类型吗?”或“我们可以使用虚拟调度吗?”而不是“我们可以让这段代码多态吗?”),和/或
- 明确定义您的术语。
尽管如此,对于成为一名出色的 C++ 程序员来说,至关重要的是了解多态性真正为您做了什么……
让您编写一次“算法”代码,然后将其应用于多种类型的数据
...然后非常清楚不同的多态机制如何满足您的实际需求。
运行时多态性适合:
Base*
由工厂方法处理的输入并作为通过s处理的异构对象集合吐出,
- 在运行时根据配置文件、命令行开关、UI 设置等选择实现,
- 实现在运行时会有所不同,例如状态机模式。
当运行时多态性没有明确的驱动程序时,编译时选项通常更可取。考虑:
- 模板类的所谓编译方面比在运行时失败的胖接口更可取
- SFINAE
- CRTP
- 优化(许多包括内联和死代码消除、循环展开、基于静态堆栈的数组与堆)
__FILE__
, __LINE__
, 字符串文字连接和宏的其他独特功能(仍然是邪恶的;-))
- 支持模板和宏测试语义使用,但不要人为地限制提供支持的方式(因为虚拟调度倾向于要求完全匹配的成员函数覆盖)
支持多态性的其他机制
正如所承诺的那样,为了完整起见,涵盖了几个外围主题:
这个答案最后讨论了上述内容如何结合以增强和简化多态代码 - 特别是参数多态性(模板和宏)。
映射到特定类型操作的机制
> 隐式编译器提供的重载
从概念上讲,编译器为内置类型重载了许多运算符。它在概念上与用户指定的重载没有区别,但因为它很容易被忽略而被列出。例如,您可以使用相同的符号添加到int
s 和s ,编译器会生成:double
x += 2
重载然后无缝扩展到用户定义的类型:
std::string x;
int y = 0;
x += 'c';
y += 'c';
编译器为基本类型提供的重载在高级 (3GL+) 计算机语言中很常见,而对多态性的明确讨论通常意味着更多。(2GLs - 汇编语言 - 通常要求程序员为不同类型显式使用不同的助记符。)
> 标准转换
C++ 标准的第四部分描述了标准转换。
第一点总结得很好(来自旧草案 - 希望仍然基本正确):
-1- 标准转换是为内置类型定义的隐式转换。Clause conv 列举了完整的此类转换集。标准转换序列是按以下顺序的标准转换序列:
[注意:标准转换序列可以为空,即它可以不包含任何转换。] 如果需要将标准转换序列应用于表达式,以将其转换为所需的目标类型。
这些转换允许以下代码:
double a(double x) { return x + 2; }
a(3.14);
a(42);
应用前面的测试:
要成为多态,[ a()
] 必须能够对至少两种不同类型(例如int
和double
)的值进行操作,找到并执行适合类型的代码。
a()
本身专门为它运行代码double
,因此不是多态的。
但是,在对编译器的第二次调用中a()
,知道为“浮点提升”(标准§4)生成适合类型的代码以转换42
为42.0
. 那个额外的代码在调用函数中。我们将在结论中讨论这一点的重要性。
> 强制、强制转换、隐式构造函数
这些机制允许用户定义的类指定类似于内置类型的标准转换的行为。我们来看一下:
int a, b;
if (std::cin >> a >> b)
f(a, b);
在这里,在std::cin
转换运算符的帮助下,对象在布尔上下文中进行评估。这可以在概念上与上述主题中的标准转换中的“整体促销”等进行分组。
隐式构造函数有效地做同样的事情,但由 cast-to 类型控制:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
编译器提供的重载、转换和强制的含义
考虑:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
如果我们希望x
在除法期间将金额视为实数(即为 6.5 而不是向下舍入为 6),我们只需更改为typedef double Amount
。
这很好,但是让代码明确地“类型正确”不会有太多的工作:
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
但是,考虑到我们可以将第一个版本转换为template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
正是由于这些小小的“便利功能”,它可以很容易地为int
or实例化double
并按预期工作。如果没有这些特性,我们将需要显式转换、类型特征和/或策略类,一些冗长、容易出错的混乱,例如:
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
因此,编译器为内置类型、标准转换、强制转换/隐式构造函数提供的运算符重载——它们都为多态性提供了微妙的支持。从这个答案顶部的定义中,他们通过映射解决“查找和执行类型适当的代码”:
“远离”参数类型
从许多数据类型多态算法代码句柄
为(可能更少)数量(相同或其他)类型编写的代码。
从常量类型的值“到”参数类型
它们不会自己建立多态上下文,但确实有助于增强/简化此类上下文中的代码。
你可能会觉得被骗了……这似乎并不多。意义在于,在参数多态上下文中(即在模板或宏内部),我们试图支持任意大范围的类型,但通常希望用其他函数、文字和为一小组类型。当操作/值在逻辑上相同时,它减少了在每种类型的基础上创建几乎相同的函数或数据的需要。这些特性相互配合,增加了一种“尽力而为”的态度,通过使用有限的可用功能和数据来做直觉上所期望的事情,并且只有在真正有歧义时才会因错误而停止。
这有助于限制对支持多态代码的多态代码的需求,围绕多态的使用绘制更紧密的网络,因此本地化使用不会强制广泛使用,并且可以根据需要提供多态的好处,而不必在编译时,在目标代码中具有相同逻辑函数的多个副本以支持使用的类型,并进行虚拟调度而不是内联或至少编译时解析的调用。与 C++ 中的典型情况一样,程序员有很大的自由来控制使用多态性的边界。