C++中的多态性 [英] Polymorphism in C++

查看:24
本文介绍了C++中的多态性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

阿法伊克:

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

C++ provides three different types of polymorphism.

  • 虚拟函数
  • 函数名重载
  • 运算符重载

除了以上三种类型的多态性,还有其他类型的多态性:

In addition to the above three types of polymorphism, there exist other kinds of polymorphism:

  • 运行时
  • 编译时
  • 临时多态性
  • 参数多态性

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

I know that runtime polymorphism can be achieved by virtual functions and static polymorphism can be achieved by template functions

但是对于另外两个

临时多态性:

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

If the range of actual types that can be used is finite and the combinations must be individually specified prior to use, this is called ad-hoc polymorphism.

参数多态性:

如果所有代码都没有提及任何特定类型,因此可以透明地与任意数量的新类型一起使用,则称为参数多态.

If all code is written without mention of any specific type and thus can be used transparently with any number of new types it is called parametric polymorphism.

我几乎无法理解它们:(

I can hardly understand them :(

如果可能的话,谁能用一个例子来解释它们?我希望这个问题的答案对他们大学的许多新通过的学生有所帮助.

can anyone explain them both if possible with an example? I hope the answers to this questions would be helpful for many new passouts from their colleges.

推荐答案

对多态性的理解/要求

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

Understanding of / requirements for polymorphism

To understand polymorphism - as the term is used in Computing Science - it helps to start from a simple test for and definition of it. Consider:

    Type1 x;
    Type2 y;

    f(x);
    f(y);

这里,f() 是要执行一些操作,并被赋予值 xy 作为输入.

Here, f() is to perform some operation and is being given values x and y as inputs.

为了表现出多态性,f() 必须能够使用至少两种 distinct 类型的值(例如 intdouble),查找并执行不同类型的代码.

To exhibit polymorphism, f() must be able to operate with values of at least two distinct types (e.g. int and double), finding and executing distinct type-appropriate code.

<小时>

C++ 多态机制

显式程序员指定的多态性

您可以编写 f() 以便它可以通过以下任何方式对多种类型进行操作:


C++ mechanisms for polymorphism

Explicit programmer-specified polymorphism

You can write f() such that it can operate on multiple types in any of the following ways:

  • 预处理:

  • Preprocessing:

#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)

  • 重载:

  • Overloading:

    void f(int& x)    { x += 2; }
    
    void f(double& x) { x += 2; }
    

  • 模板:

  • Templates:

    template <typename T>
    void f(T& x) { x += 2; }
    

  • 虚拟调度:

  • Virtual dispatch:

    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
    

  • 为了完整性,编译器提供的内置类型、标准转换和强制转换/强制转换的多态性将在后面讨论:

    Compiler-provided polymorphism for builtin types, Standard conversions, and casting/coercion are discussed later for completeness as:

    • 无论如何,它们通常都被直观地理解(保证哦,那个"的反应),
    • 它们会影响要求和无缝使用上述机制的门槛,并且
    • 解释是对更重要概念的干扰.

    鉴于以上的多态机制,我们可以按多种方式对其进行分类:

    Given the polymorphic mechanisms above, we can categorise them in various ways:

    • 什么时候选择多态类型特定代码?

    • When is the polymorphic type-specific code selected?

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

    • Run time means the compiler must generate code for all the types the program might handle while running, and at run-time the correct code is selected (virtual dispatch)
    • Compile time means the choice of type-specific code is made during compilation. A consequence of this: say a program only called f above with int arguments - depending on the polymorphic mechanism used and inlining choices the compiler might avoid generating any code for f(double), or generated code might be thrown away at some point in compilation or linking. (all mechanisms above except virtual dispatch)

    支持哪些类型?

    • Ad-hoc 意味着您提供明确的代码来支持每种类型(例如重载、模板专业化);您明确添加了for this"(根据 ad hoc 的含义)类型、其他一些this",也许还有that"的支持 ;-).
    • 参数意味着您可以尝试将函数用于各种参数类型,而无需专门执行任何操作来启用对它们的支持(例如模板、宏).具有类似于模板/宏的函数/运算符的对象期望1 模板/宏完成其工作所需的全部内容,确切的类型无关紧要.C++20 引入的概念"表达并强制执行此类期望 - 请参阅 cppreference 页面在这里.

    • Ad-hoc meaning you provide explicit code to support each type (e.g. overloading, template specialisation); you explicitly add support "for this" (as per ad hoc's meaning) type, some other "this", and maybe "that" too ;-).
    • Parametric meaning you can just try to use the function for various parameter types without specifically doing anything to enable its support for them (e.g. templates, macros). An object with functions/operators that act like the template/macro expects1 is all that template/macro needs to do its job, with the exact type being irrelevant. The "concepts" introduced by C++20 express and enforce such expectations - see cppreference page here.

    • 参数多态性提供了鸭子类型——这个概念归功于 James Whitcomb Riley,他显然说过 当我看到一只像鸭子一样走路和像鸭子一样游泳的鸟和嘎嘎叫时就像一只鸭子,我称那只鸟为鸭子.".

    • Parametric polymorphism provides duck typing - a concept attributed to James Whitcomb Riley who apparently said "When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.".

    template <typename Duck>
    void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
    
    do_ducky_stuff(Vilified_Cygnet());
    

    子类型(又名包含)多态性允许您在不更新算法/函数的情况下处理新类型,但它们必须派生自同一个基类(虚拟分派)

    Subtype (aka inclusion) polymorphism allows you to work on new types without updating the algorithm/function, but they must be derived from the same base class (virtual dispatch)

    1 - 模板非常灵活.SFINAE(另见 std::enable_if) 有效地允许对参数多态性的几组期望.例如,您可以编码,当您正在处理的数据类型具有 .size() 成员时,您将使用一个函数,否则另一个不需要 .size 的函数() (但可能以某种方式受到影响 - 例如使用较慢的 strlen() 或不在日志中打印有用的消息).您还可以在使用特定参数实例化模板时指定临时行为,或者保留一些参数参数 (部分模板专业化) 与否 (完全专业化).

    1 - Templates are extremely flexible. SFINAE (see also std::enable_if) effectively allows several sets of expectations for parametric polymorphism. For example, you might encode that when the type of data you're processing has a .size() member you'll use one function, otherwise another function that doesn't need .size() (but presumably suffers in some way - e.g. using the slower strlen() or not printing as useful a message in the log). You can also specify ad-hoc behaviours when the template is instantiated with specific parameters, either leaving some parameters parametric (partial template specialisation) or not (full specialisation).

    Alf Steinbach 评论说,在 C++ 标准中,polymorphic 仅指使用虚拟分派的运行时多态性.一般比较科学.根据 C++ 创建者 Bjarne Stroustrup 的词汇表 (http://www.stroustrup.com/glossary.html):

    Alf Steinbach comments that in the C++ Standard polymorphic only refers to run-time polymorphism using virtual dispatch. General Comp. Sci. meaning is more inclusive, as per C++ creator Bjarne Stroustrup's glossary (http://www.stroustrup.com/glossary.html):

    多态性 - 为不同类型的实体提供单一接口.虚函数通过基类提供的接口提供动态(运行时)多态性.重载的函数和模板提供静态(编译时)多态性.TC++PL 12.2.6、13.6.1、D&E 2.9.

    polymorphism - providing a single interface to entities of different types. Virtual functions provide dynamic (run-time) polymorphism through an interface provided by a base class. Overloaded functions and templates provide static (compile-time) polymorphism. TC++PL 12.2.6, 13.6.1, D&E 2.9.

    这个答案 - 就像问题一样 - 将 C++ 功能与 Comp.科学.术语.

    This answer - like the question - relates C++ features to the Comp. Sci. terminology.

    C++ 标准使用比 Comp 更窄的多态性"定义.科学.社区,以确保您的观众的相互理解考虑...

    With the C++ Standard using a narrower definition of "polymorphism" than the Comp. Sci. community, to ensure mutual understanding for your audience consider...

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

    不过,要成为一名出色的 C++ 程序员,关键是了解多态性真正为您做什么...

    Still, what's crucial to being a great C++ programmer is understanding what polymorphism's really doing for you...

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

        letting you write "algorithmic" code once and then apply it to many types of data

    ...然后非常清楚不同的多态机制如何满足您的实际需求.

    ...and then be very aware of how different polymorphic mechanisms match your actual needs.

    运行时多态套装:

    • 由工厂方法处理的输入,并作为通过 Base*s 处理的异构对象集合吐出,
    • 在运行时根据配置文件、命令行开关、UI 设置等选择实现,
    • 实现在运行时会有所不同,例如状态机模式.
    • input processed by factory methods and spat out as an heterogeneous object collection handled via Base*s,
    • implementation chosen at runtime based on config files, command line switches, UI settings etc.,
    • implementation varied at runtime, such as for a state machine pattern.

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

    When there's not a clear driver for run-time polymorphism, compile-time options are often preferable. Consider:

    • 模板类的所谓编译方面优于在运行时失败的胖接口
    • SFINAE
    • CRTP
    • 优化(许多包括内联和死代码消除、循环展开、静态基于堆栈的数组与堆)
    • __FILE____LINE__、字符串文字连接和其他宏的独特功能(仍然是邪恶的;-))
    • 支持模板和宏测试语义使用,但不要人为限制提供支持的方式(因为虚拟调度倾向于要求完全匹配的成员函数覆盖)
    • the compile-what's-called aspect of templated classes is preferable to fat interfaces failing at runtime
    • SFINAE
    • CRTP
    • optimisations (many including inlining and dead code elimination, loop unrolling, static stack-based arrays vs heap)
    • __FILE__, __LINE__, string literal concatenation and other unique capabilities of macros (which remain evil ;-))
    • templates and macros test semantic usage is supported, but don't artificially restrict how that support is provided (as virtual dispatch tends to by requiring exactly matching member function overrides)

    正如承诺的那样,为了完整起见,涵盖了几个外围主题:

    As promised, for completeness several peripheral topics are covered:

    • 编译器提供的重载
    • 转化次数
    • 强制转换/强制

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

    This answer concludes with a discussion of how the above combine to empower and simplify polymorphic code - especially parametric polymorphism (templates and macros).

    >编译器提供的隐式重载

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

    Conceptually, the compiler overloads many operators for builtin types. It's not conceptually different from user-specified overloading, but is listed as it's easily overlooked. For example, you can add to ints and doubles using the same notation x += 2 and the compiler produces:

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

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

    Overloading then seamlessly extends to user-defined types:

    std::string x;
    int y = 0;
    
    x += 'c';
    y += 'c';
    

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

    Compiler-provided overloads for basic types is common in high-level (3GL+) computer languages, and explicit discussion of polymorphism generally implies something more. (2GLs - assembly languages - often require the programmer to explicitly use different mnemonics for different types.)

    >标准转换

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

    The C++ Standard's fourth section describes Standard conversions.

    第一点总结得很好(来自旧草稿 - 希望仍然基本正确):

    The first point summarises nicely (from an old draft - hopefully still substantially correct):

    -1- 标准转换是为内置类型定义的隐式转换.Clause conv 列举了完整的此类转换集.标准转换序列是按以下顺序进行的标准转换序列:

    -1- Standard conversions are implicit conversions defined for built-in types. Clause conv enumerates the full set of such conversions. A standard conversion sequence is a sequence of standard conversions in the following order:

    • 从以下集合中进行零次或一次转换:左值到右值的转换、数组到指针的转换和函数到指针的转换.

      • Zero or one conversion from the following set: lvalue-to-rvalue conversion, array-to-pointer conversion, and function-to-pointer conversion.

        以下集合中的零个或一个转换:整数提升、浮点提升、整数转换、浮点转换、浮点整数转换、指针转换、指向成员的指针转换和布尔转换.

        Zero or one conversion from the following set: integral promotions, floating point promotion, integral conversions, floating point conversions, floating-integral conversions, pointer conversions, pointer to member conversions, and boolean conversions.

        零或一次资格转换.

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

        [Note: a standard conversion sequence can be empty, i.e., it can consist of no conversions. ] A standard conversion sequence will be applied to an expression if necessary to convert it to a required destination type.

        这些转换允许以下代码:

        These conversions allow code such as:

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

        应用之前的测试:

        要成为多态,[a()] 必须能够使用至少两种 distinct 类型的值(例如 intdouble),查找并执行适合类型的代码.

        To be polymorphic, [a()] must be able to operate with values of at least two distinct types (e.g. int and double), finding and executing type-appropriate code.

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

        a() itself runs code specifically for double and is therefore not polymorphic.

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

        But, in the second call to a() the compiler knows to generate type-appropriate code for a "floating point promotion" (Standard §4) to convert 42 to 42.0. That extra code is in the calling function. We'll discuss the significance of this in the conclusion.

        > 强制、强制转换、隐式构造函数

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

        These mechanisms allow user-defined classes to specify behaviours akin to builtin types' Standard conversions. Let's have a look:

        int a, b;
        
        if (std::cin >> a >> b)
            f(a, b);
        

        这里,对象 std::cin 在一个布尔上下文中被求值,在转换运算符的帮助下.这可以在概念上与上述主题中的标准转换中的整体促销"等进行分组.

        Here, the object std::cin is evaluated in a boolean context, with the help of a conversion operator. This can be conceptually grouped with "integral promotions" et al from the Standard conversions in the topic above.

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

        Implicit constructors effectively do the same thing, but are controlled by the cast-to type:

        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 金额.

        If we want the amount x to be treated as a real number during the division (i.e. be 6.5 rather than rounded down to 6), we only need change to typedef double Amount.

        这很好,但是太多要让代码明确地类型正确":

        That's nice, but it wouldn't have been too much work to make the code explicitly "type correct":

        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;
        }                                      }
        

        但是,考虑到我们可以将第一个版本转换为模板:

        But, consider that we can transform the first version into a template:

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

        正是由于这些小小的便利功能",它可以很容易地为 intdouble 实例化并按预期工作.如果没有这些特性,我们将需要显式类型转换、类型特征和/或策略类,以及一些冗长、容易出错的混乱,例如:

        It's due to those little "convenience features" that it can be so easily instantiated for either int or double and work as intended. Without these features, we'd need explicit casts, type traits and/or policy classes, some verbose, error-prone mess like:

        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;
        }
        

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

        So, compiler-provided operator overloading for builtin types, Standard conversions, casting / coercion / implicit constructors - they all contribute subtle support for polymorphism. From the definition at the top of this answer, they address "finding and executing type-appropriate code" by mapping:

        • 远离"参数类型

        • "away" from parameter types

        • 来自多种数据类型的多态算法代码句柄

        • from the many data types polymorphic algorithmic code handles

        为(可能更少)数量(相同或其他)类型编写的代码.

        to code written for a (potentially lesser) number of (the same or other) types.

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

        "to" parametric types from values of constant type

        他们确实自己建立多态上下文,但确实有助于增强/简化此类上下文中的代码.

        They do not establish polymorphic contexts by themselves, but do help empower/simplify code inside such contexts.

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

        You may feel cheated... it doesn't seem like much. The significance is that in parametric polymorphic contexts (i.e. inside templates or macros), we're trying to support an arbitrarily large range of types but often want to express operations on them in terms of other functions, literals and operations that were designed for a small set of types. It reduces the need to create near-identical functions or data on a per-type basis when the operation/value is logically the same. These features cooperate to add an attitude of "best effort", doing what's intuitively expected by using the limited available functions and data and only stopping with an error when there's real ambiguity.

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

        This helps limit the need for polymorphic code supporting polymorphic code, drawing a tighter net around the use of polymorphism so localised use doesn't force widespread use, and making the benefits of polymorphism available as needed without imposing the costs of having to expose implementation at compile time, have multiple copies of the same logical function in the object code to support the used types, and in doing virtual dispatch as opposed to inlining or at least compile-time resolved calls. As is typical in C++, the programmer is given a lot of freedom to control the boundaries within which polymorphism is used.

        这篇关于C++中的多态性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆