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

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

问题描述

AFAIK:



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




  • 虚拟功能

  • 功能名称重载

  • 操作员重载



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




  • 运行时

  • 编译时

  • 自组织多态性

  • 参数多态性



我知道运行时多态性可以通过虚函数
静态多态性



但是对于其他两个




  • ad-hoc多态性

  • 参数多态性
    网站说



ad-hoc多态性 / p>

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



参数多态性



如果所有代码都没有提及任何特定类型,透明地使用任何数量的新类型它被称为参数多态性。



我几乎不能理解他们:(



$ b我希望这个问题的答案对于许多来自这些大学的新的passouts是有帮助的。

解决方案

多态性



要理解多态性 - 作为计算科学中使用的术语,它有助于从简单的测试和定义开始考虑:

  Type1 x; 
Type2 y;

f(x);
f b $ b

这里, f()操作,并且给定值 x y 作为输入为了显示多态性, f )必须能够使用至少两个不同类型的值操作(例如 int double ),找到并执行不同类型适当的代码。



多态性的C ++机制



显式程序员指定的多态性



可以写 f()以下列任何方式输入:




  • 预处理:

      #define f(X)((X)+ = 2)
    //(注意:在实际代码中,为宏使用更长的大写名称!


  • 重载:

      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; } //运行时多态分派




相关机制



编译器提供的内建类型,标准转换和强制转换的多态性稍后将作为完整性讨论:




  • 他们通常直观地理解(保证哦,那反应),


  • 解释是对更重要的概念的一种分心。



术语



进一步分类



鉴于上述多态机制,




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




    • 运行时表示编译器必须为运行时程序可能处理的所有类型生成代码,并在运行时选择正确的代码( virtual dispatch

    • 编译时间意味着在编译期间选择特定于类型的代码。这样做的结果是:一个程序只使用 int 参数调用 f ,这取决于使用的多态机制和内联选项,编译器可能会避免为 f(double)生成任何代码,或者生成的代码可能在编译或链接的某个时刻丢弃。 (除虚拟分派之外的每个机制




  • 支持哪些类型?




    • Ad-hoc 表示您提供显式代码以支持每种类型模板专业化);您明确添加支持for this(根据 ad hoc 的含义)类型,一些其他this,也可以that; - )。

    • 参数意味着您可以尝试对各种参数类型使用该函数,而无需专门对其进行任何支持(例如模板,宏)。具有类似于template / macro的函数/运算符的对象期望 1 是所有模板/宏需要完成其工作,确切的类型是不相关的。从C ++ 11中截取的概念有助于表达和实施这些期望 - 让我们希望它们能进入下一个标准。




      • p>参数多态性提供鸭式打字 - 一个归因于James Whitcomb Riley的概念,他显然说:当我看到一只鸟像鸭子一样行走,像鸭子一样游动,

         模板< typename Duck> 
        void do_ducky_stuff(const Duck& x){x.walk()。swim()。quack(); }

        do_ducky_stuff(Vilified_Cygnet());



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





1 - SFINAE 有效地允许几组期望。例如,你可以编码,当你正在处理的数据类型有一个 .size()成员,你将使用一个函数,否则另一个函数不需要 .size()(但是可能会有某种方式 - 例如使用较慢的 strlen()在日志中有用的消息)。



多态性



Alf Steinbach评论说, C ++标准多态仅指使用虚拟分派的运行时多态性。一般化合物。 Sci。意义更具包容性,如C ++创建者Bjarne Stroustrup的词汇表( http://www.stroustrup.com/glossary.html ) ):


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


到比较。 Sci。术语



讨论



使用多态性 Sci。社群,以确保观众的相互理解...




  • 使用明确的用语这个代码可以重用于其他类型?或我们可以使用虚拟分派吗?而不是我们可以让这个代码多态吗?)和/或

  • 清楚地定义你的术语。 / li>


不过,作为一个优秀的C ++程序员,至关重要的是理解



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

>

...然后非常了解不同的多态机制如何匹配您的实际需求。



运行时多态性适合:




  • 输入由工厂方法处理,并作为异构对象集合处理,通过 Base * s,

  • 在运行时根据配置文件,命令行开关,UI设置等选择实现

  • 例如对于状态机模式。



当没有运行时多态性的驱动程序时,编译时选项。考虑:




  • 编译模板类的方面比fat接口在运行时失败更好。

  • SFINAE

  • CRTP

  • 优化(其中包括内联和死代码清除,循环展开, li>
  • __ FILE __ __ LINE __ ,字符串文字连接以及宏的其他独特功能保持邪恶; - ))

  • 支持模板和宏测试语义使用,但不会人为地限制如何提供支持(因为虚拟分派倾向于需要匹配成员函数声明)



支持多态性的其他机制



正如所承诺的,




  • 编译器提供的重载

  • 转换

  • casts / coercion



文档最后讨论了如何将这些组合起来并简化多态代码 - 特别是参数多态和宏)。



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



>隐式编译器提供重载



从概念上讲,编译器重载许多内置类型的操作符。它在概念上不同于用户指定的重载,但是列出它很容易被忽略。例如,您可以使用相同的符号 x +添加到 int s和 double = 2 ,并且编译器生成:




  • 特定于类型的CPU指令

  • <
    $ b $ p

    重载则无法扩展到用户定义的类型:

      std :: string x; 
    int y = 0;

    x + ='c';
    y + ='c';

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



    >标准转换 b
    $ b

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



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


    -1-标准转换是为内置类型定义的隐式转换。子句转换列举了这些转换的完整集合。标准转换序列是标准转换序列,顺序如下:





    • 零或从以下集合进行一次转换:从值到值的转换,数组到指针的转换和函数到指针的转换。


    • 从下列集合中进行零或一次转换:积分促销,浮点促销,积分转换,浮点积分转换,指针转换到成员转换和布尔转换。


    • 零个或一个认证转换。





    [注意:标准转换序列可以为空,没有转化。 ]如果需要将标准转换序列应用于表达式,以将其转换为所需的目标类型。


    这些转换允许的代码如下:

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

    a(3.14);
    a(42);

    应用之前的测试:


    为了多态性,[ a()]必须能够操作至少两个不同类型的值 int double ),查找并执行类型适当的代码

    a()本身运行 double

    但是,在第二次调用 a()编译器知道为浮点提升(标准§4)生成类型适当的代码,将 42 转换为 42.0 。该额外代码在调用函数中。



    >强制,强制转换,隐式构造函数 b $ b

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

      int a,b; 

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

    这里,对象 std :: cin 在转换操作符的帮助下在布尔上下文中求值。这可以在概念上与整体促销等等从上面的主题中的标准转换中分组。



    隐式构造函数有效地做同样的事情, -to type:

      f(const std :: string& x); 
    f(hello); //调用`std :: string :: string(const char *)`



    提供超载,转换和强制。



    请考虑:

     
    {
    typedef int 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金额;
    金额x = 13;金额x = 13.0;
    x / = 2; x / = 2.0;
    std :: cout<< double(x)* 1.1; std :: cout< x * 1.1;但是,考虑到我们可以将第一个版本转换为一个<$ c






    $ b < $ c> 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;
    }



    因此,编译器提供的内置类型的操作符重载,标​​准转换,强制/隐式构造函数 - 它们都为多态性提供了微妙的支持。从这个答案顶部的定义,他们通过映射:




    • 找到并执行类型适当的代码

      多种数据类型多态算法代码处理

      的参数类型

    • $ b $ b

  • 到来自常量类型值的参数类型




他们不是自己建立多态上下文,而是帮助授权/简化上下文中的代码。



可能会感觉被骗...它似乎没有多少。重要的是,在参数多态上下文(即在模板或宏中),我们试图支持任意大范围的类型,但通常希望在其他函数,字面和操作方面表达它们的操作,小套类型。当操作/值在逻辑上相同时,它减少了在每个类型的基础上创建近似相同的功能或数据的需要。这些功能合作增加了尽力而为的态度,通过使用有限的可用功能和数据,做出直观的预期,并且只有当有真正的歧义时才会出现错误。



这有助于限制对支持多态性代码的多态性代码的需求,在多态性的使用周围绘制更紧密的网络,因此本地化使用不强制广泛使用,并且使得多态性的益处在需要时可用,而没有强加必须暴露实现的成本在编译时,在目标代码中具有相同逻辑功能的多个副本以支持使用的类型,并且在进行虚拟分派而不是内联或至少编译时解析调用时。正如在C ++中典型的,程序员被赋予了很多自由来控制使用多态性的边界。


AFAIK:

C++ provides three different types of polymorphism.

  • Virtual functions
  • Function name overloading
  • Operator overloading

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

  • run-time
  • compile-time
  • ad-hoc polymorphism
  • parametric polymorphism

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

But for the other two

  • ad-hoc polymorphism
  • parametric polymorphism the website says,

ad-hoc polymorphism:

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.

parametric 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 thier 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);

Here, f() is to perform some operation and is being given values x and y as inputs. 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++ 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
    

Other related mechanisms

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

  • they're commonly intuitively understood anyway (warranting a "oh, that" reaction),
  • they impact the threshold in requiring, and seamlessness in using, the above mechanisms, and
  • explanation is a fiddly distraction from more important concepts.

Terminology

Further categorisation

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

  • When is the polymorphic type-specific code selected?

    • 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. (every mechanism except virtual dispatch)

  • Which types are supported?

    • 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" cut from C++11 help express and enforce such expectations - let's hope they make it into the next Standard.

      • 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 effectively allows several sets of expectations. 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). The functions may still employ parametric polymorphism / duck-typing.

"Polymorphic"

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):

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.

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

Discussion

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

  • using unambiguous terminology ("can we make this code reusable for other types?" or "can we use virtual dispatch?" rather than "can we make this code polymorphic?"), and/or
  • clearly defining your terminology.

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.

Run-time polymorphism suits:

  • 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:

  • 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 matching member function declarations)

Other mechanisms supporting polymorphism

As promised, for completeness several peripheral topics are covered:

  • compiler-provided overloads
  • conversions
  • casts/coercion

The document concludes with a discussion of how these combine to empower and simplify polymorphic code - especially parametric polymorphism (templates and macros).

Mechanisms for mapping to type-specific operations

> Implicit compiler-provided overloads

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:

  • type-specific CPU instructions
  • a result of the same type.

Overloading then seemlessly extends to user-defined types:

std::string x;
int y = 0;

x += 'c';
y += 'c';

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

> Standard conversions

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

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

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

  • Zero or one qualification conversion.

[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);

Applying the earlier test:

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() itself runs code specifically for double and is therefore not polymorphic.

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.

> Coercion, casts, implicit constructors

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

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*)`

Implications of compiler-provided overloads, conversions and coercion

Consider:

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

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

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.

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