对多态性的了解/要求
要理解多态性(在计算机科学中使用该术语),它有助于从对其进行简单的测试和定义开始。考虑:
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 就是模板/宏完成其工作所需的全部内容,而确切的类型无关紧要。C ++ 20引入的“概念”表达并实施了这种期望-请参见cppreference页面。
参数多态性提供了鸭子的输入方式 -归因于James Whitcomb Riley的一个概念,他显然说过:“当我看到一只鸟像鸭子走路,像鸭子一样游泳,像鸭子一样duck时,我称那只鸟为鸭子。” 。
template <typename Duck>
void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
do_ducky_stuff(Vilified_Cygnet());
子类型(又名包含)多态允许您在不更新算法/功能的情况下处理新类型,但是它们必须派生自相同的基类(虚拟调度)
1-模板非常灵活。 SFINAE(另请参见std::enable_if
)有效地实现了对参数多态性的几套期望。例如,您可能会编码为:当您正在处理的数据类型具有.size()
成员时,您将使用一个函数,否则将不需要另一个函数.size()
(但可能会受到某种影响-例如,使用较慢strlen()
或不打印在日志中有用的消息)。您还可以在使用特定参数实例化模板时指定临时行为,使某些参数保持参数化(部分模板特殊化)或不保留参数(完全特殊化)。
“多态的”
Alf Steinbach评论说,在C ++ Standard中,多态只涉及使用虚拟调度的运行时多态。通用电脑 科学 根据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设置等选择的实现,
- 实现在运行时会有所不同,例如针对状态机模式。
如果没有明确的运行时多态性驱动程序,则通常首选编译时选项。考虑:
- 模板类的所谓编译方面胜于在运行时失败的胖接口
- FINEA
- CRTP
- 优化(许多内容包括内联和无效代码消除,循环展开,基于静态堆栈的数组与堆)
__FILE__
、、__LINE__
字符串文字串联和宏的其他独特功能(仍然是邪恶的;-))
- 模板和宏测试是否支持语义用法,但不要人为地限制提供支持的方式(因为虚拟分派往往要求完全匹配的成员函数覆盖)
其他支持多态的机制
如所承诺的,为了完整起见,涵盖了几个外围主题:
该答案以对以上内容如何结合以赋能和简化多态代码(尤其是参数多态(模板和宏))的讨论结束。
映射到特定于类型的操作的机制
>隐式编译器提供的重载
从概念上讲,编译器会为内置类型重载许多运算符。从概念上讲,它与用户指定的重载没有什么区别,但由于容易被忽略而被列出。例如,您可以使用相同的符号添加到int
s和double
s x += 2
,编译器将生成:
然后,重载无缝扩展到用户定义的类型:
std::string x;
int y = 0;
x += 'c';
y += 'c';
编译器为基本类型提供的重载在高级(3GL +)计算机语言中很常见,对多态性的明确讨论通常意味着更多。(2GL(汇编语言)通常要求程序员针对不同的类型显式使用不同的助记符。)
>标准转换
C ++标准的第四部分介绍了标准转换。
第一点很好地总结了(从一个旧的草稿中-希望仍然是正确的):
-1-标准转换是为内置类型定义的隐式转换。条款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
借助转换运算符在布尔上下文中评估对象。可以从上述主题中的“标准转化”中将其与“整体促销”等概念地分组。
隐式构造函数可以有效地执行相同的操作,但是受强制转换类型控制:
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
或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 ++中的典型做法一样,程序员拥有很大的自由度来控制使用多态性的边界。