操作符重载在没有引用的情况下可以工作吗?

Could operator overloading have worked without references?

本文关键字:工作 情况下 重载 引用 操作符      更新时间:2023-10-16

根据Bjarne Stroustrup的说法,在c++中引入引用是为了支持操作符重载:

引入

引用主要是为了支持操作符重载。

C按值传递每个函数参数,在按值传递对象效率低下或不合适的地方,用户可以传递指针。此策略不适用于使用操作符重载的情况。在这种情况下,表示法的便利性是必不可少的,因为如果对象很大,就不能期望用户插入地址操作符。例如:

a = b - c;

是可以接受的(即传统的)符号,但是

a = &b - &c;

不是。无论如何,&b - &c在C中已经有了意义,我不想改变它。

在语言中同时使用指针和引用是c++新手经常感到困惑的原因。

Bjarne不可以通过引入一个特殊的语言规则来解决这个问题吗?如果存在一个接受指针的用户定义操作符函数,该规则允许对象参数衰减为指针。

减法的声明和使用应该是这样的:

Foo operator-(const Foo* x, const Foo* y);
a = b - c;

这样的解决方案曾经被提出/考虑过吗?它会有什么严重的缺点吗?

是的,我知道,引用由于其限制而提供了其他优势,但这不是重点。

有趣的是,C中带有类的赋值操作符似乎就是这样工作的:

改变类对象的赋值含义[…]通过声明一个名为operator=的类成员函数来完成。例如:

class x {
public:
    int a;
    class y * p;
    void operator = (class x *);
};

自动转换是c++的祸根。让我们都给Bjarne Stroustrup写一封感谢的电子邮件,感谢他没有添加从对象到指针的广泛自动转换。

如果你寻找一个技术上的原因:在c++中,指针的减法,甚至是指向用户定义类型的指针,都已经定义好了。此外,这将导致操作符的重载版本在每次拷贝接受对象时产生歧义。

我不明白这如何解决问题:指针上调用的operator-已经有意义了。

您将为Foo*参数定义operator-,但它已经存在。

那么你需要一些人为的语义,"当用指针显式调用时,将调用指针算术运算符。当使用对象左值调用时,它们衰变成指针,然后调用重载版本的指针。老实说,这似乎比仅仅添加参考要做作得多,也不那么直观。

然后在operator-中,我将参数作为指针,然后如果我需要从那里调用另一个(或相同的)操作符,我最好确保对这些操作符进行解引用。否则,我就会意外地执行指针运算。

你正在提议一个隐式转换,它的语义与你自己做的转换不同。即:

Foo foo;
bar(foo); 

执行从FooFoo*的隐式转换,然后调用签名为void bar(Foo*)的函数。

但是这段代码会做一些完全不同的事情:

Foo foo;
bar(&foo);

也将转换为Foo*,并且它还将调用具有void bar(Foo*)签名的函数,但它可能是一个不同的函数。(以operator-为例,一个是用户重载操作符,另一个是标准指针算术操作符。

然后考虑模板代码。一般来说,隐式转换使模板非常痛苦,因为传入的类型可能不是您想要操作的类型。

当然,还有更多的实际问题:

引用可以实现指针无法实现的优化。(编译器可以假设它们永远不会为空,并且它可能能够更好地进行混叠分析)

此外,如果找不到匹配的operator-,代码将静默失败。编译器不会告诉我,而是隐式地开始做指针运算。这并不妨碍交易,但我真的真的更喜欢尽可能早地发现错误。

c++的目标之一是使其泛型:避免拥有"神奇"的类型或函数。在现实世界的c++中,操作符只是具有不同语法的函数。有了这个规则,它们也会有不同的语义。(如果用函数语法调用操作符会怎样?operator+(a, b) ?

c++的目标是强类型,所以尝试与这一理念保持一致,对象不是指向该对象的指针是有意义的,我完全同意sbi关于没有自动转换是多么伟大(我想到了多贡献者项目)。

为了更具体地解决你的问题,学习c++的人一开始可能会对引用和指针感到困惑,但我不确定它会为他们澄清任何事情,让这种自动转换为他们发生。
例如:

Foo ** operator+(Foo **lhs, Foo **rhs)
{...}
Foo *varFoo1,*varFoo2;
varFoo1 + &varFoo2;

在假设的隐式对象到指针之后,varFoo1应该被接受为期望Foo **的方法的参数吗?因为该方法需要指向Foo *varFoo1 *is-a* Foo *的指针。

引用作为实参的另一个优点:
const引用可以接受右值作为参数(例如经典的字符串字面值),而指针则不能。

如果您接受使用指针(隐式或显式)进行操作会引入真正令人困惑的语义(我是在指针上操作还是在指针上操作?)那么没有引用只会留下按值调用。(除了你的Foo c = &b - &a;的例子考虑的情况下,你想写一个操作符,确实使用指针作为它的参数之一)

我不认为没有&的指针在调用点是有用的-可行的,当然没有比引用更好。如果你让它成为operator s的一个特殊功能,并且只有操作符,那么这种行为就进入了"神秘"的特殊情况领域。如果你想通过使它成为一个通用特性来减少它的"特殊情况"方面,那么我认为它对目前的引用调用没有帮助或有用。

例如,如果我想写一个带Foovoid*的运算符,我可以这样写:

Foo operator+(const Foo& f, void *ptr);

在你提议的规则下,它将变成:Foo operator+(Foo *f, void *ptr);。我所看到的问题是,即使我希望ptr通过"隐式&规则"显式地成为指针,它也会接受任何,并且似乎没有办法禁止自动转换。所以double d; Foo f; f = f + d;int i; Foo f; f = f + i;可能会以两种不同的方式匹配!

按值调用可能对"简单"类型有效且有意义,在您确实不想获取每个操作数副本的情况下,您可以使用智能指针。总的来说,与基于引用的方法相比,被迫复制所有东西的想法似乎非常不干净。

我从来没有读过D&E的书,但我的理解是,添加引用不是为了让它在向函数传递参数时看起来更好,而是为了让它在从中产生值时看起来更好。在内置类型上使用简单和复合赋值操作符和下标操作符时,结果都是左值,但是对于没有引用的内置类型则无法做到这一点。

我还想知道如何实现既对类型又对指向类型的指针进行操作的操作符。

struct Foo { };
struct Bar { };
Foo operator +(Foo&, Bar*);
Bar operator +(Foo*, Bar&);
Foo operator +(Bar*, Foo&);
Bar operator +(Bar&, Foo*);

struct Foo { };
struct Bar { };
Foo operator +(Foo*, Bar*);
Bar operator +(Foo*, Bar*);
Foo operator +(Bar*, Foo*);
Bar operator +(Bar*, Foo*);

同样的事情可以只用一种类型显示,但在这种情况下,简单地说"不要那样做"似乎是合理的…当涉及多个类型时,意外引入歧义的可能性会增加。