C++中的多态性

Vij*_*jay 124 c++ polymorphism c++-faq

据我所知:

C++提供了三种不同类型的多态性.

  • 虚拟功能
  • 函数名称重载
  • 运算符重载

除了上述三种类型的多态性外,还存在其他种类的多态性:

  • 运行
  • 编译时间
  • ad-hoc多态性
  • 参数多态性

我知道运行时多态性可以通过虚函数实现 ,静态多态可以通过模板函数实现

但对于另外两个

ad-hoc多态性:

如果可以使用的实际类型的范围是有限的,并且必须在使用之前单独指定组合,则这称为ad-hoc多态.

参数多态性:

如果所有代码都是在没有提及任何特定类型的情况下编写的,因此可以透明地使用任意数量的新类型,这称为参数多态.

我几乎无法理解他们:(

任何人都可以用一个例子解释他们两个吗?我希望这些问题的答案对他们大学的许多新的消息有所帮助.

Ton*_*roy 213

了解/多态性的要求

要理解多态性 - 正如计算科学中使用的术语 - 从简单的测试和定义开始是有帮助的.考虑:

    Type1 x;
    Type2 y;

    f(x);
    f(y);
Run Code Online (Sandbox Code Playgroud)

这里,f()是执行一些操作,并给出值xy输入.

要展示多态性,f()必须能够使用至少两种不同类型(例如intdouble)的值进行操作,找到并执行不同类型的代码.


多态的C++机制

显式程序员指定的多态性

您可以编写f()这样的文件,它可以通过以下任何方式在多种类型上运行:

其他相关机制

编译器提供的内置类型,标准转换和转换/强制的多态性将在后面讨论完整性,如下所示:

  • 无论如何,它们通常被直观地理解(保证" 哦,那个 "反应),
  • 它们影响了要求的门槛,以及使用上述机制的无缝性,以及
  • 解释是对更重要概念的一种分散注意力.

术语

进一步分类

鉴于上述多态机制,我们可以通过各种方式对它们进行分类:

  • 何时选择了多态类型特定代码?

    • 运行时意味着编译器必须为程序在运行时可能处理的所有类型生成代码,并且在运行时选择正确的代码(虚拟调度)
    • 编译时间意味着在编译期间选择特定于类型的代码.这样做的结果是:假设一个程序只f在上面用int参数调用 - 取决于所使用的多态机制和内联选择,编译器可能会避免生成任何代码f(double),或者生成的代码可能会在编译或链接的某些时候丢弃.(除虚拟调度外的所有上述机制)

  • 支持哪些类型?

    • Ad-hoc意味着您提供显式代码来支持每种类型(例如重载,模板特化); 你明确地添加支持"为此"(根据ad hoc的意思)类型,其他一些"这个",也许"那个"也是;-).
    • 参数含义你可以尝试将函数用于各种参数类型,而无需专门做任何事情来支持它们(例如模板,宏).具有与模板/宏一样的函数/运算符的对象期望1 模板/宏需要完成其工作的所有对象,其确切类型无关紧要.从C++ 11中删除的"概念"有助于表达和实施这些期望 - 让我们希望它们成为后来的标准.

      • 参数多态性提供了鸭子打字 - 这个概念归功于James Whitcomb Riley,他明显地说:"当我看到一只像鸭子一样走路的小鸟像鸭子一样游动,像鸭子一样呱呱叫时,我称这只鸟为鸭子." .

        template <typename Duck>
        void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
        
        do_ducky_stuff(Vilified_Cygnet());
        
        Run Code Online (Sandbox Code Playgroud)
    • 子类型(又名包含)多态性允许您在不更新算法/函数的情况下处理新类型,但它们必须从相同的基类派生(虚拟调度)

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++标准使用比"Comporp"更窄的"多态"定义.科学.社区,以确保您的受众的相互理解考虑...

  • 使用明确的术语("我们可以使这些代码可以重用于其他类型吗?"或"我们可以使用虚拟调度吗?"而不是"我们可以使这段代码具有多态性吗?")和/或
  • 明确定义您的术语.

但是,成为一名优秀的C++程序员至关重要的是理解多态性真正为你做的事情......

    让你编写一次"算法"代码,然后将其应用于许多类型的数据

...然后非常清楚不同的多态机制如何与您的实际需求相匹配.

运行时多态性适合:

  • 输入由工厂方法处理并吐出作为通过Base*s 处理的异构对象集合,
  • 基于配置文件,命令行开关,UI设置等在运行时选择的实现,
  • 实现在运行时变化,例如状态机模式.

当没有明确的运行时多态性驱动程序时,编译时选项通常更可取.考虑:

  • 模板化类的compile-what-c​​alled方面比运行时失败的fat接口更可取
  • SFINAE
  • CRTP
  • 优化(许多包括内联和死代码消除,循环展开,基于静态堆栈的数组vs堆)
  • __FILE__,, __LINE__字符串文字串联和宏的其他独特功能(仍然是邪恶的;-))
  • 支持模板和宏测试语义使用,但不要人为地限制提供支持的方式(因为虚拟调度往往需要完全匹配的成员函数覆盖)

支持多态性的其他机制

正如所承诺的,为了完整性,涵盖了几个外围主题:

  • 编译器提供的重载
  • 转换
  • 铸就/胁迫

这个答案最后讨论了上述如何结合使用和简化多态代码 - 特别是参数多态(模板和宏).

映射到特定于类型的操作的机制

>隐式编译器提供的重载

从概念上讲,编译器会为内置类型重载许多运算符.它在概念上与用户指定的重载不同,但是因为它很容易被忽略而被列出.例如,您可以使用相同的表示法添加到ints和doubles x += 2,编译器会生成:

  • 特定于类型的CPU指令
  • 相同类型的结果.

然后重载无缝扩展到用户定义的类型:

std::string x;
int y = 0;

x += 'c';
y += 'c';
Run Code Online (Sandbox Code Playgroud)

编译器提供的基本类型的重载在高级(3GL +)计算机语言中很常见,并且对多态性的明确讨论通常意味着更多.(2GL - 汇编语言 - 通常要求程序员明确地为不同类型使用不同的助记符.)

>标准转换

C++标准的第四部分描述了标准转换.

第一点很好地总结了(从一个旧的草案 - 希望仍然基本上正确):

-1-标准转换是为内置类型定义的隐式转换.条款转义列举了全套此类转换.标准转换序列是一系列标准转换,顺序如下:

  • 来自以下集合的零或一次转换:左值到右值的转换,数组到指针的转换以及函数到指针的转换.

  • 来自以下集合的零或一次转换:整数促销,浮点促销,积分转换,浮点转换,浮点积分转换,指针转换,成员转换指针和布尔转换.

  • 零或一个资格转换.

[注意:标准转换序列可以为空,即它可以不包含任何转换.]如果需要,将标准转换序列应用于表达式,以将其转换为所需的目标类型.

这些转换允许以下代码:

double a(double x) { return x + 2; }

a(3.14);
a(42);
Run Code Online (Sandbox Code Playgroud)

应用早期测试:

要具有多态性,[ a()]必须能够使用至少两种不同类型(例如intdouble)的值进行操作,找到并执行适合类型的代码.

a()本身专门为代码运行代码double,因此不是多态的.

但是,在对a()编译器的第二次调用中,知道要为转换42为"浮点提升"(标准§4)生成适合类型的代码42.0.额外的代码在调用函数中.我们将在结论中讨论这一点的重要性.

>胁迫,演员,隐含的构造者

这些机制允许用户定义的类指定类似于内置类型的标准转换的行为.我们来看一下:

int a, b;

if (std::cin >> a >> b)
    f(a, b);
Run Code Online (Sandbox Code Playgroud)

这里,在std::cin转换运算符的帮助下,在布尔上下文中计算对象.这可以在概念上与来自上述主题中的标准转换的"整体促销"等组合.

隐式构造函数有效地做同样的事情,但是由强制转换类型控制:

f(const std::string& x);
f("hello");  // invokes `std::string::string(const char*)`
Run Code Online (Sandbox Code Playgroud)

编译器提供的重载,转换和强制的含义

考虑:

void f()
{
    typedef int Amount;
    Amount x = 13;
    x /= 2;
    std::cout << x * 1.1;
}
Run Code Online (Sandbox Code Playgroud)

如果我们希望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;
}                                      }
Run Code Online (Sandbox Code Playgroud)

但是,请考虑我们可以将第一个版本转换为template:

template <typename Amount>
void f()
{
    Amount x = 13;
    x /= 2;
    std::cout << x * 1.1;
}
Run Code Online (Sandbox Code Playgroud)

这是由于那些小"便利功能",它是这么简单实例化要么intdouble工作按预期.没有这些功能,我们需要显式的强制转换,类型特征和/或策略类,一些冗长,容易出错的混乱,如:

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;
}
Run Code Online (Sandbox Code Playgroud)

因此,编译器提供的运算符重载内置类型,标准转换,转换/强制/隐式构造函数 - 它们都为多态性提供了微妙的支持.从这个答案顶部的定义,他们通过映射来解决"查找和执行类型适当的代码":

  • 从参数类型"离开"

    • 来自众多数据类型的多态算法代码处理

    • 为(相同或其他)类型的(潜在较小)数目编写的代码.

  • 从常量类型的值"到"参数类型

它们本身并不建立多态上下文,但确实有助于在这种上下文中赋予/简化代码.

你可能会觉得被骗了......看起来并不多.重要的是,在参数化多态上下文(即内部模板或宏)中,我们试图支持任意大范围的类型,但通常希望根据为其设计的其他函数,文字和操作来表达对它们的操作.一小组类型.当操作/值在逻辑上相同时,它减少了在每种类型的基础上创建几乎相同的功能或数据的需要.这些功能相互配合,增加了"尽力而为"的态度,通过使用有限的可用功能和数据来做直觉预期的事情,并且只有在存在真正的模糊性时才停止错误.

这有助于限制对支持多态代码的多态代码的需求,围绕多态性的使用绘制更紧密的网络,因此本地化使用不会强制广泛使用,并且可以根据需要提供多态性的好处,而不必承担必须暴露实现的成本编译时,在目标代码中具有相同逻辑功能的多个副本以支持使用的类型,并且在进行虚拟分派时与内联或至少编译时解析的调用相反.正如C++中的典型情况一样,程序员可以自由地控制使用多态的边界.


Ste*_*314 15

在C++中,重要的区别是运行时绑定和编译时绑定.Ad-hoc与参数并没有多大帮助,我稍后会解释.

|----------------------+--------------|
| Form                 | Resolved at  |
|----------------------+--------------|
| function overloading | compile-time |
| operator overloading | compile-time |
| templates            | compile-time |
| virtual methods      | run-time     |
|----------------------+--------------|
Run Code Online (Sandbox Code Playgroud)

注意 - 运行时多态性仍然可以在编译时解析,但这只是优化.需要有效地支持运行时解决方案,并与其他问题进行权衡,这是导致虚拟功能成为现实的一部分.这对于C++中所有形式的多态性来说都是非常关键的 - 每一种都来自不同背景下的不同权衡取舍.

函数重载和运算符重载在各方面都是相同的.使用它们的名称和语法不会影响多态性.

模板允许您一次指定许多函数重载.

还有另一组名称用于同一分辨时间的想法......

|---------------+--------------|
| early binding | compile-time |
| late binding  | run-time     |
|---------------+--------------|
Run Code Online (Sandbox Code Playgroud)

这些名称与OOP更相关,因此说模板或其他非成员函数使用早期绑定有点奇怪.

为了更好地理解虚函数和函数重载之间的关系,理解"单调度"和"多调度"之间的区别也很有用.这个想法可以被理解为一个进步......

  • 首先,有单形函数.函数的实现由函数名唯一标识.没有参数是特殊的.
  • 然后,有单一的派遣.其中一个参数被认为是特殊的,并使用(以及名称)来标识要使用的实现.在OOP中,我们倾向于将此参数视为"对象",在函数名称之前列出它等.
  • 然后,有多个派遣.任何/所有参数都有助于确定要使用的实现.因此,再一次,没有一个参数需要特殊.

OOP显然比提名一个特殊参数的借口更多,但这只是其中的一部分.回顾我所说的权衡 - 单一调度很容易有效(通常的实现称为"虚拟表").多次调度更加尴尬,不仅在效率方面,而且在单独编译方面.如果你很好奇,你可能会查找"表达问题".

正如对非成员函数使用术语"早期绑定"有点奇怪,使用术语"单一调度"和"多次调度"有点奇怪,其中多态性在编译时被解析.通常,C++被认为不具有多个分派,这被认为是一种特定的运行时分辨率.但是,函数重载可以看作是在编译时完成的多次调度.

回到参数化和ad-hoc多态性,这些术语在函数式编程中更受欢迎,并且它们在C++中不太起作用.即使是这样...

参数多态意味着您将类型作为参数,并且无论您使用哪种类型的参数,都会使用完全相同的代码.

Ad-hoc多态性是ad-hoc,因为您根据特定类型提供不同的代码.

重载和虚函数都是ad-hoc多态的例子.

再次,有一些同义词......

|------------+---------------|
| parametric | unconstrained |
| ad-hoc     | constrained   |
|------------+---------------|
Run Code Online (Sandbox Code Playgroud)

除了这些不是完全同义词之外,尽管它们通常被视为它们,并且这就是C++中可能出现混淆的地方.

将这些视为同义词的原因在于,通过将多态性约束到特定类型的类,可以使用特定于这些类类型的操作.这里的"类"一词可以在OOP意义上解释,但实际上只是指(共同命名)共享某些操作的类型集.

因此,通常采用参数多态(至少在默认情况下)来暗示不受约束的多态性.因为无论类型参数如何都使用相同的代码,所以唯一可支持的操作是适用于所有类型的操作.通过保留不受约束的类型集,您严格限制可应用于这些类型的操作集.

在例如Haskell中,你可以......

myfunc1 :: Bool -> a -> a -> a
myfunc1 c x y = if c then x else y
Run Code Online (Sandbox Code Playgroud)

a是一个不受约束的多态类型.它可以是任何东西,所以我们对这种类型的值的处理力度不大.

myfunc2 :: Num a => a -> a
myfunc2 x = x + 3
Run Code Online (Sandbox Code Playgroud)

在这里,a被约束为Num类的成员- 类似数字的类型.该约束允许您使用这些值执行数字操作,例如添加它们.即使3是多态 - 类型推断也表明你的意思3是类型a.

我认为这是受约束的参数多态.只有一个实现,但它只能在受限情况下应用.临时方面是选择+3使用.每个"实例" Num都有自己独特的实现.因此即使在Haskell中,"参数化"和"不受约束"也不是真正的同义词 - 不要怪我,这不是我的错!

在C++中,重载和虚函数都是ad-hoc多态.ad-hoc多态的定义并不关心是在运行时还是在编译时选择实现.

如果每个模板参数都有类型,C++与模板的参数多态非常接近typename.有类型参数,无论使用哪种类型,都有一个实现.但是,"替换失败不是错误"规则意味着由于在模板中使用操作而产生隐式约束.其他复杂性包括用于提供替代模板的模板专业化 - 不同(ad-hoc)实现.

因此,在某种程度上,C++具有参数多态性,但它是隐式约束的,并且可以被ad-hoc替代方法覆盖 - 即这种分类对C++并不真正有用.