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

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

问题描述

AFAIK:

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

但是对于另外两个

临时多态性:

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

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() 必须能够处理至少两种 不同 类型的值(例如 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 意味着您提供明确的代码来支持每种类型(例如重载、模板特化);您明确添加了为此"(根据 ad hoc 的含义)类型、其他一些这个",也许还有那个"的支持 ;-)
    • 参数意味着您可以尝试将该函数用于各种参数类型,而无需专门做任何事情来启用对它们的支持(例如模板、宏).一个具有函数/操作符的对象就像模板/宏期望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++ 标准中,多态 仅指使用虚拟分派的运行时多态.一般比较科学.根据 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- 标准转换是为内置类型定义的隐式转换.子句 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()] 必须能够处理至少两种 不同 类型的值(例如 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.额外的代码位于调用函数中.我们将在结论中讨论这一点的重要性.

        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 Amount.

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

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

        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天全站免登陆