Vij*_*jay 124 c++ polymorphism c++-faq
据我所知:
C++提供了三种不同类型的多态性.
除了上述三种类型的多态性外,还存在其他种类的多态性:
我知道运行时多态性可以通过虚函数实现 ,静态多态可以通过模板函数实现
但对于另外两个
ad-hoc多态性:
如果可以使用的实际类型的范围是有限的,并且必须在使用之前单独指定组合,则这称为ad-hoc多态.
参数多态性:
如果所有代码都是在没有提及任何特定类型的情况下编写的,因此可以透明地使用任意数量的新类型,这称为参数多态.
我几乎无法理解他们:(
任何人都可以用一个例子解释他们两个吗?我希望这些问题的答案对他们大学的许多新的消息有所帮助.
Ton*_*roy 213
要理解多态性 - 正如计算科学中使用的术语 - 从简单的测试和定义开始是有帮助的.考虑:
Type1 x;
Type2 y;
f(x);
f(y);
Run Code Online (Sandbox Code Playgroud)
这里,f()
是执行一些操作,并给出值x
和y
输入.
要展示多态性,
f()
必须能够使用至少两种不同类型(例如int
和double
)的值进行操作,找到并执行不同类型的代码.
您可以编写f()
这样的文件,它可以通过以下任何方式在多种类型上运行:
预处理:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Run Code Online (Sandbox Code Playgroud)重载:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Run Code Online (Sandbox Code Playgroud)模板:
template <typename T>
void f(T& x) { x += 2; }
Run Code Online (Sandbox Code Playgroud)虚拟发货:
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
Run Code Online (Sandbox Code Playgroud)编译器提供的内置类型,标准转换和转换/强制的多态性将在后面讨论完整性,如下所示:
鉴于上述多态机制,我们可以通过各种方式对它们进行分类:
何时选择了多态类型特定代码?
f
在上面用int
参数调用 - 取决于所使用的多态机制和内联选择,编译器可能会避免生成任何代码f(double)
,或者生成的代码可能会在编译或链接的某些时候丢弃.(除虚拟调度外的所有上述机制)
支持哪些类型?
参数含义你可以尝试将函数用于各种参数类型,而无需专门做任何事情来支持它们(例如模板,宏).具有与模板/宏一样的函数/运算符的对象期望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 处理的异构对象集合,当没有明确的运行时多态性驱动程序时,编译时选项通常更可取.考虑:
__FILE__
,, __LINE__
字符串文字串联和宏的其他独特功能(仍然是邪恶的;-))正如所承诺的,为了完整性,涵盖了几个外围主题:
这个答案最后讨论了上述如何结合使用和简化多态代码 - 特别是参数多态(模板和宏).
>隐式编译器提供的重载
从概念上讲,编译器会为内置类型重载许多运算符.它在概念上与用户指定的重载不同,但是因为它很容易被忽略而被列出.例如,您可以使用相同的表示法添加到int
s和double
s x += 2
,编译器会生成:
然后重载无缝扩展到用户定义的类型:
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()
]必须能够使用至少两种不同类型(例如int
和double
)的值进行操作,找到并执行适合类型的代码.
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)
这是由于那些小"便利功能",它是这么简单实例化要么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;
}
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显然比提名一个特殊参数的借口更多,但这只是其中的一部分.回顾我所说的权衡 - 单一调度很容易有效(通常的实现称为"虚拟表").多次调度更加尴尬,不仅在效率方面,而且在单独编译方面.如果你很好奇,你可能会查找"表达问题".
正如对非成员函数使用术语"早期绑定"有点奇怪,使用术语"单一调度"和"多次调度"有点奇怪,其中多态性在编译时被解析.通常,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++并不真正有用.
归档时间: |
|
查看次数: |
49913 次 |
最近记录: |