在 C++ 中哪种多态性被认为更惯用?

发布于 2024-09-07 20:02:33 字数 1715 浏览 5 评论 0原文

C++ 似乎不是一种面向价值的语言很好地支持面向对象(以及子类型多态性)。至于参数多态性,缺乏类型参数的类型推断和冗长的模板语法使得它们使用起来具有挑战性。

请注意,我唯一了解的语言是 Java(子类型多态性)和 Haskell(参数多态性)。两种语言都倾向于一种多态性。然而,C++(在某种程度上)支持两者,但两者似乎都可以在我认为不直观的情况下工作。因此,当使用 C++ 进行编程时,我很难决定应该以何种方式进行编码。

所以我的问题是,在 C++ 中哪种多态性被认为更惯用?

编辑1:

我的“C++不能很好地支持OO”观点的解释:

动态方法分派和LSP在OO中很常见,不是吗?但是,当涉及到 C++ 时,在不借助指针(原始指针或智能指针)的情况下应用这些技术是不可能的(或不切实际的)。
例如,考虑一个带有虚拟方法print的类Person,该方法将他的名字打印到控制台。让另一个类 Student 扩展 Person 并重写 print 以打印他的姓名和学校名称。

现在考虑以下函数:

void blah(const Person & p) {
  p.print();
}

这里,如果我传递一个 Student 对象,print 方法将从 Person 调用 print,不是来自学生。因此它违背了子类型多态性的基本思想。

现在我知道在这种情况下我可以使用动态分配(即指针)来实现子类型多态性。然而静态分配在 C++ 中更常见。指针被用作最后的手段(我记得在这里的其他线程中读过它)。所以我发现很难调和推荐静态分配而不是动态分配的良好实践(这就是我说 C++ 是有价值的)面向)与子类型多态性。

当使用 Java 时,我倾向于使用动态分配,因此子类型多态性在那里是很自然的。然而,C++ 的情况并非如此,

希望我的观点现在已经清楚了。

编辑2:

好的,我在编辑1中给出的例子是错误的。但我的观点仍然有效,而且我已经多次遇到过这个问题。我无法在脑海中回忆起所有这些案例。

这是我想到的一个案例。

在 Java 中,您可以在类中引用超类型,然后使它们指向其任何子类型的实例。

例如,

class A {
  B y1;
  B y2;
}

abstract class B {
  // yada yada
}

class B1 exyends B {
  // yada yada
}

class B2 extends B {
  // yada yada
}

这里 A 中的引用 y1y2 可以指向 B1 的实例, B2B 的任何其他子类。 C++ 引用不能重新分配。所以我必须在这里使用指针。所以这证明了在 C++ 中不使用指针就不可能实现各种子类型多态性。

C++ being a value oriented language doesn't seem to support OO (and thus sub-typing polymorphism) very well. As for parametric polymorphism, lack of type inference on type parameters and verbose syntax of templates makes them challenging to use.

Please note that the only languages I know moderately well are Java (sub-typing polymorphism) and Haskell (parametric polymorphism). Both languages are leaned towards one kind of polymorphism. However C++ supports both (to some extent), but both seem to work in a matter that I find unintuitive. So when programming in C++ I have a pretty hard time in deciding what way I should exactly code.

So my question is what kind of polymorphism is considered more idiomatic in C++?

EDIT 1:

Explanation of my "C++ doesn't support OO well" point:

Dynamic method dispatch and LSP are very common in OO, aren't they? But when it comes to C++, applying these techniques without resorting to pointers (raw or smart) is not possible (or practical).

For example,consider a class Person with virtual method print which prints his name to the console. Let there be another class Student that extends Person and overrides print to print his name plus his school's name.

Now consider the following function:

void blah(const Person & p) {
  p.print();
}

Here if I pass a Student object, print method would invoke print from Person, not from Student. Thus it defies the very basic idea of subtyping polymorphism.

Now I am aware that I can use dynamic allocation (i.e. pointers) to achieve subtyping polymorphism in this case. However static allocation is more common in C++. Pointers are used as last resort (I remember having read it in some other thread here).So I find it difficult it difficult to reconcile the Good Practices that recommend static allocation over dynamic allocation (this is what I meant when I said C++ is value oriented) with subtyping polymorphism.

When using Java, I tend to use dynamic allocation all over and thus subtyping polymorphism is quite natural there. This is not the case with C++ however,

Hope my point is clear now.

EDIT 2:

Okay, the example I gave in my edit 1 is wrong. But my point is still valid and I have faced the problem many times. I am unable to recall all those cases top of my head.

Here's one case that comes to my mind.

In Java you can have reference of super type in your classes and then make them point to instances of any of its subtypes.

For example,

class A {
  B y1;
  B y2;
}

abstract class B {
  // yada yada
}

class B1 exyends B {
  // yada yada
}

class B2 extends B {
  // yada yada
}

Here the references y1 and y2 in A can be made to point to instances of either B1, B2 or any other subclass of B. C++ references cannot be reassigned. So I will have to use pointers here. So this provs that in C++ it's not possible to achieve all sorts of subtyping polymorphism without using pointers.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

夜司空 2024-09-14 20:02:33

添加了第五次重新开放投票后,我有机会成为第一个添加另一个回复的人。让我们从 C++ 不能很好地支持 OO 的说法开始。给出的例子是:

现在考虑以下函数:

void blah(const Person & p) {
  p.print();
}

在这里,如果我传递一个 Student 对象,print 方法将从 Person 调用 print,而不是从
学生。因此它违背了子类型多态性的基本思想。

长话短说,这个例子完全是错误的——或者更准确地说,关于这个例子的主张是错误的。如果将 Student 对象传递给此函数,则将调用 Student::print而不是 Person::print如上所述。因此,C++ 完全按照 OP 的意愿实现了多态性。

唯一不符合 C++ 习惯的部分是,您通常使用 operator<< 来打印对象,因此不要使用 print (显然)仅打印到 < code>std::cout,你可能应该让它接受一个参数,而不是 blah,重载 operator<<,类似于:

std::ostream &operator<<(std::ostream &os, Person const &p) { 
    return p.print(os);
}

现在, 可以创建一个像描述的那样的blah,但要做到这一点,你必须让它按值获取参数:

void blah(Person p) { 
    p.print();
}

所以有原始声明在某种程度上是正确的——具体来说,当/如果你想使用多态性,你确实需要使用指针或引用。

但请注意,这与分配对象的方式无关(仅是外围关系)。无论所讨论的对象是如何分配的,您都可以通过引用传递。如果函数采用指针,则可以传递自动或静态分配的对象的地址。如果它需要引用,您可以传递动态分配的对象。

就类型推断而言,C++ 有函数模板,但没有类模板。 C++0x 添加了 decltypeauto 的新含义(它一直是保留字,但自 C 诞生以来几乎从未使用过),允许类型推断更广泛的各种情况。它还添加了 lambda(缺少它确实是当前 C++ 的一个严重问题),它可以使用 auto。仍然存在不支持类型推断的情况,但这会很好——但至少在我看来,auto(特别是)大大减少了这种情况。

就冗长而言,毫无疑问它至少部分是正确的。有点像 Java,您编写 C++ 的舒适程度至少在某种程度上取决于编辑器,该编辑器包含各种“技巧”(例如,代码完成)以帮助减少您的键入量。 Haskell 在这方面表现出色——Haskell 可以让您比几乎任何其他语言完成更多的每个字符输入任务(APL 是少数明显的例外之一)。同时,值得注意的是,“泛型”(Java 或 C# 中的)与 C++ 模板一样冗长,但通用性远不如 C++ 模板。就冗长程度而言,C++ 介于(或接近)一个极端的 Haskell 和处于(或接近)另一个极端的 Java 和 C# 之间。

回到最初的问题,哪个更常用:曾经有一段时间,C++ 没有模板,所以本质上你唯一的选择就是子类型化。正如您可能猜到的那样,当时它被大量使用,即使它并不是真正的最佳选择。

C++ 拥有模板已经很长时间了。模板现在如此普遍,以至于它们基本上是不可避免的。就比如IOStreams,原来只使用继承,现在也使用了模板。标准容器、迭代器和算法都大量使用模板(并且完全避免继承)。

因此,较旧的代码(以及来自较旧或更保守的编码人员的新代码)往往主要或完全集中于子类型。更新的和/或更自由编写的代码往往更多地使用模板。至少根据我的经验,最近的大多数代码都混合使用了两者。在这两者之间,我通常会在必要时使用子类型,但在模板可以完成这项工作时更喜欢模板。

编辑:显示多态性的演示代码:

#include <iostream>

class Person { 
public:
    virtual void print() const { std::cout << "Person::print()\n"; }
};

class Student : public Person { 
public:
    virtual void print() const { std::cout << "Student::print()\n"; }
};

void blah(const Person &p) { 
    p.print();
}

int main() { 
    Student s;
    blah(s);
    return 0;
}

结果(从我的计算机上运行上面的代码剪切并粘贴,用 MS VC++ 编译):

Student::print()

所以是的,它完全按照您想要的方式实现多态性 - 请注意,在这个示例中,对象问题是在堆栈上分配的,而不是使用new

编辑2:(回应问题的编辑):

确实,您无法分配给参考。但这与多态性问题是正交的——(例如)你想要分配的内容与初始化的类型是相同还是不同并不重要,你都不能以任何方式进行分配。

至少对我来说,很明显,引用和指针之间的功能肯定存在一些差异,否则就没有理由添加对该语言的引用。如果要分配它们来引用不同的对象,则需要用户指针,而不是引用。一般来说,如果可以的话我会使用引用,如果必须的话我会使用指针。至少在我看来,作为类成员的引用通常充其量是高度可疑的(例如,这意味着您无法分配该类型的对象)。底部:如果您想要引用的作用,请务必使用引用 - 但抱怨因为引用不是指针似乎(至少对我来说)没有多大意义。

Having added the fifth vote to reopen gives me a chance at being the first to add another reply. Let's start with the claim that C++ doesn't support OO well. The example given is:

Now consider the following function:

void blah(const Person & p) {
  p.print();
}

Here if I pass a Student object, print method would invoke print from Person, not from
Student. Thus it defies the very basic idea of subtyping polymorphism.

To make a long story short, this example is just plain wrong -- or more accurately, the claim made about the example is wrong. If you pass a Student object to this function, what will be invoked will be Student::print, not Person::print as claimed above. Thus, C++ implements polymorphism exactly as the OP apparently wishes.

The only part of this that isn't idiomatic C++ is that you normally use operator<< to print out objects, so instead of print (apparently) printing only to std::cout, you should probably have it take a parameter, and instead of blah, overload operator<<, something like:

std::ostream &operator<<(std::ostream &os, Person const &p) { 
    return p.print(os);
}

Now, it is possible to create a blah that would act as described, but to do so you'd have to have it take its parameter by value:

void blah(Person p) { 
    p.print();
}

So there is some degree of truth to the original claim -- specifically, when/if you want to use polymorphism, you do need to use pointers or references.

Note, however, that this isn't related (any more than peripherally) to how you allocate objects. You can pass by reference regardless of how the object in question was allocated. If a function takes a pointer, you can pass the address of an automatically or statically allocated object. If it takes a reference, you can pass a dynamically allocated object.

As far as type inference goes, C++ has it for function templates, but not class templates. C++0x adds decltype and a new meaning for auto (which has been a reserved word, but essentially never used almost since the dawn of C) that allow type inference for a wider variety of situations. It also adds lambdas (the lack of which really is a serious problem with the current C++), which can use auto. There are still situations where type inference isn't supported, but would be nice -- but at least IMO, auto (in particular) reduces that quite a bit.

As far as verbosity goes, there's little question that it's at least partly true. Somewhat like Java, your degree of comfort in writing C++ tends to depend to at least some degree on an editor that includes various "tricks" (e.g., code completion) to help reduce the amount you type. Haskell excels in this respect -- Haskell lets you accomplish more per character typed than almost any other language around (APL being one of the few obvious exceptions). At the same time, it's worth noting that "generics" (in either Java or C#) are about as verbose, but much less versatile than C++ templates. In terms of verbosity, C++ stands somewhere between Haskell at (or close to) one extreme, and Java and C# at (or, again, close to) the opposite extreme.

Getting to the original question of which is used more often: there was a time when C++ didn't have templates, so essentially your only choice was subtyping. As you can probably guess, at that time it was used a lot, even when it wasn't really the best choice.

C++ has had templates for a long time now. Templates are now so common that they're essentially unavoidable. Just for example, IOStreams, which originally used only inheritance, now also use templates. The standard containers, iterators, and algorithms all use templates heavily (and eschew inheritance completely).

As such, older code (and new code from coders who are older or more conservative) tends to concentrate primarily or exclusively on subtyping. Newer and/or more liberally written code, tends to use templates more. At least in my experience, most reasonably recent code uses a mixture of both. Between the two, I'll normally use subtyping when I have to, but prefer templates when they can do the job.

Edit: demo code showing polymorphism:

#include <iostream>

class Person { 
public:
    virtual void print() const { std::cout << "Person::print()\n"; }
};

class Student : public Person { 
public:
    virtual void print() const { std::cout << "Student::print()\n"; }
};

void blah(const Person &p) { 
    p.print();
}

int main() { 
    Student s;
    blah(s);
    return 0;
}

result (cut and pasted from running code above on my computer, compiled with MS VC++):

Student::print()

So yes, it does polymorphism exactly as you'd want -- and note that in this example, the object in question is allocated on the stack, not using new.

Edit 2: (in response to edit of question):

It's true that you can't assign to a reference. That's orthogonal to questions of polymorphism though -- it doesn't matter (for example) whether what you want to assign is of the same or different type from what it was initialized with, you can't do an assignment either way.

At least to me, it would seem obvious that there must be some difference in capabilities between references and pointers, or there would have been no reason to add references to the language. If you want to assign them to refer to different objects, you need to user pointers, not references. Generally speaking, I'd use a reference when you can, and a pointer if you have to. At least IMO, a reference as a class member is usually highly suspect at best (e.g., it means you can't assign objects of that type). Bottom: if you want what a reference does, by all means use a reference -- but complaining because a reference isn't a pointer doesn't seem (at least to me) to make much sense.

人生戏 2024-09-14 20:02:33

两种选择都有其优点。 Java 风格的继承在现实世界的 C++ 代码中更为常见。由于您的代码通常必须与其他代码兼容,因此我将首先关注子类型多态性,因为这是大多数人所熟知的。

另外,您应该考虑多态性是否真的是表达问题解决方案的正确方法。人们常常在不必要的情况下构建复杂的继承树。

Both options have their advantages. Java-style inheritance is far more common in real world C++ code. Since your code typically has to play well with others, I would focus on subtyping polymorphism first since that's what most people know well.

Also, you should consider whether polymorphism is really the right way to express the solution to your problems. Far too often people build elaborate inheritance trees when they aren't necessary.

二智少女猫性小仙女 2024-09-14 20:02:33

模板在编译时通过创建模板化函数或对象的副本来评估。如果您在运行时需要多态性(即:时不时地使用 std::vectorvecvec->push_back(new Derived()) ...)你被迫使用子类型和虚拟方法。

编辑:我想我应该提出模板更好的情况。模板是“开放的”,因为模板化函数或对象将与您尚未创建的类一起使用......只要这些类适合您的界面。例如,auto_ptr<>适用于我可以制作的任何类,即使标准库设计者并没有真正考虑过我的类。类似地,模板化算法(例如逆向)可以在任何支持取消引用的类上工作,以及运算符++。

使用子类型时,您必须在某处写下类层次结构。也就是说,您需要在代码中的某处声明 B extends A...然后才能像 A 一样使用 B。另一方面,您不必说 B Implements Randomaccessiterator 使其能够与模板化代码一起使用。

在少数情况下,两者都满足您的要求,然后使用您更喜欢使用的那个。根据我的经验,这种情况并不经常发生。

Templates are evaluated at compile-time by creating basically a copy of the templated function or object. If you need polymorphism during runtime (ie: a std::vector<Base*> vec and vec->push_back(new Derived()) every now and then...) you're forced to use subtypes and virtual methods.

EDIT: I guess I should put forward the case where Templates are better. Templates are "open", in that a templated function or object will work with classes that you haven't made yet... so long as those classes fit your interface. For example, auto_ptr<> works with any class I can make, even though the standard library designers haven't thought of my classes really. Similarly, the templated algorithms such as reverse work on any class that supports dereferencing, and operator++.

When using subtyping, you have to write down the class hierarchy somewhere. That is, you need to say B extends A somewhere in your code... before you can use B like A. On the other hand, you DON'T have to say B implements Randomaccessiterator for it to work with templated code.

In the few situations where both satisfy your requirements, then use the one you're more comfortable with using. In my experience, this situation doesn't happen very often.

夏日浅笑〃 2024-09-14 20:02:33

“吸”是一个相当强烈的词。也许我们需要考虑一下 Stroustrup 使用 C++ 的目标

C++ 的设计考虑了某些原则。其中:
它必须向后兼容 C。
它不应该限制程序员做他们想做的事。
您不应该为不使用的东西付费。

因此,第一个标准制定了一个相当严格的标准——C 中合法的所有内容在 C++ 中编译时都必须有效(并且具有相同的效果)。正因为如此,包括了许多必要的妥协。
另一方面,C++ 也给了你很多权力(或者,正如对 C 的描述,“足够的绳子来吊死你自己。”)权力伴随着责任,如果你选择,编译器不会与你争论做一些愚蠢的事情。

我现在承认,自从我上次查看 Haskell 以来已经有大约 15 年了,所以我对此有点生疏 - 但参数多态性(完全类型安全)总是可以在 C++ 中被覆盖。
子类型化多态性可以。本质上,任何东西都可以被覆盖——如果你坚持将一种指针类型转换为另一种类型(无论多么疯狂),编译器不会与你争论。

因此,解决了这个问题后,C++ 确实提供了许多具有多态性的选项。
经典的公共继承模型“is-a”——子类化。这很常见。

受保护的继承模型“is-implemented-in-terms-of”(继承实现,但不继承接口)

私有继承模型“is-implemented-using”(包含实现)

后两者不太常见。聚合(在类内部创建类实例)更为常见,而且通常更灵活。

但 C++ 还支持多重继承(真正的实现和接口多重继承),以及重复继承带来的所有固有的复杂性和风险(可怕的菱形模式) - 以及处理该问题的方法。
(如果您有兴趣,Scott Myers 的“有效 C++”和“更有效 C++”将有助于理清其中的复杂性。)

我不相信 C++ 是一种排除其他事物的“面向价值的语言”。 C++ 几乎可以成为你想要的样子。你只需要知道你想要什么,以及如何让它做到。
并不是说C++很烂,而是C++很锋利,很容易割伤自己。

'Suck' is a pretty strong term. Perhaps we need to think about what Stroustrup was aiming for with C++

C++ was designed with certain pricniples in mind. Amongst others:
It had to be backwards compatible with C.
It shouldn't restrict the programmer from doing what they wanted to.
You shouldn't pay for things you don't use.

So, that first one makes a pretty stiff standard to stick to - everything that was legal in C had to work (and work with the same effect) when compiled in C++. Because of this, a lot of necessary compromises were included.
On the other hand, C++ also gives you a lot of power (or, as it has been described for C, 'enough rope to hang yourself.') With power comes responsibility, and the compiler won't argue with you if you choose to do something stupid.

I'll admit now, it's been about 15 years since I last looked at Haskell, so I'm a bit rusty on that - but parametric polymorphism (full type safety) can always be overridden in C++.
Subtyping polymorphism can to. Esentially, anything can be overridden - the compiler won't argue with you if you insist on casting one pointer type to another (no matter how insane.)

So, having got that out of the way, C++ does give lots of options with polymorphism.
The classic public inheritance models 'is-a' - sub-classing. It's very common.

Protected inheritance models 'is-implemented-in-terms-of' (inheriting implementation, but not interface)

Private inheritance models 'is-implemented-using' (containing the implementation)

The latter two are much less common. Aggregation (creating a class instance inside the class) is much more common, and often more flexible.

But C++ also supports multiple inheritance (true implementation and interface multiple inheritance), with all the inherent complexity and risks of repeated inheritance that brings (the dreaded diamond pattern) - and also ways of dealing with that.
(Scott Myers 'Effective C++' and 'More Effective C++' will help untangle the compexities if you're interested.)

I'm not convinced that C++ is a 'value oriented language' to the exclusion of other things. C++ can be what you want it to be, pretty much. You just need to know what it is you want, and how to make it do it.
It's not that C++ sucks, so much as C++ is very sharp, and you can easily cut yourself.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文