C ++中的默认按引用传递语义
编辑:这个问题更多的是关于语言工程,而不是C ++本身。我以C ++为例说明我想要的东西,主要是因为我每天都在使用它。我不想知道它如何在C ++上工作,但开始讨论如何实现。
那不是现在的工作方式,那是我希望可以完成的方式,肯定会破坏C的兼容性,但这就是我认为外部" C"的意义所在。
我的意思是,在我们现在声明的每个函数或者方法中,我们都必须显式地编写该对象,该对象将通过在其前面引用引用运算符的引用来发送。我希望每个非POD类型都可以通过引用自动发送,因为我经常使用每个对象,每个对象的大小超过32位,而这几乎是我的所有类。
让我们举例说明现在的情况,假设a,b和c为类:
class example { public: int just_use_a(const a &object); int use_and_mess_with_b(b &object); void do_nothing_on_c(c object); };
现在,我希望:
class example { public: int just_use_a(const a object); int use_and_mess_with_b(b object); extern "C" void do_nothing_on_c(c object); };
现在,do_nothing_on_c()的行为可以像今天一样。
至少对我来说这很有趣,感觉要清楚得多,而且如果我们知道每个非POD参数都是通过引用来传递的,我相信错误与我们必须显式声明它的情况相同。
对于此更改的另一种观点,对于来自C的人来说,引用运算符在我看来是获取变量地址的一种方法,这就是我用来获取指针的方法。我的意思是,它是同一个运算符,但在不同的上下文中具有不同的语义,这对我们来说也感觉有点不对吗?
解决方案
老实说,我认为C ++中按价值传递/按引用传递的整体是令人误解的。一切都是通过价值传递的。我们有以下三种情况:
- 在哪里传递变量的本地副本
void myFunct(int cantChangeMyValue)
- 在哪里将指针的本地副本传递给变量
void myFunct(int* cantChangeMyAddress) { *cantChangeMyAddress = 10; }
- 在传递"引用"的地方,但是通过编译器魔术,就好像我们传递了一个指针并且每次都只是对其取消引用一样。
void myFunct(int & hereBeMagic) { hereBeMagic = 10; // same as 2, without the dereference }
我个人发现,记住一切都是通过价值传递的,这一点要少得多。在某些情况下,该值可能是一个地址,使我们可以在函数外部进行更改。
建议将不允许程序员执行第1个操作。我个人认为,放弃该选项将是一个坏主意。 C / C ++的一大优点是具有精细的内存管理。使所有内容都通过引用传递只是在尝试使C ++更像Java。
是的,我认为这是一个非常令人困惑的重载。
这是微软对这种情况不得不说的:
Do not confuse reference declarations with use of the address-of operator. When & identifier is preceded by a type, such as int or char, then identifier is declared as a reference to the type. When & identifier is not preceded by a type, the usage is that of the address-of operator.
我在C或者C ++方面并不是很出色,但与在汇编器中进行编码相比,我在整理*和&在两种语言中的各种用法时遇到了更大的麻烦。
对于我来说,完全改变一段代码含义的编译器选项听起来像是一个非常糟糕的主意。习惯使用C ++语法或者查找其他语言。
最好的建议是养成思考我们真正想要发生的事情的习惯。当我们没有副本构造函数(或者不想使用它)时,按引用传递是很好的选择,并且对于大型对象而言,这样做更便宜。但是,然后在类之外感觉到参数的突变。相反,我们可以通过const引用传递-那么没有任何突变,但是我们不能进行本地修改。传递廉价对象的const值传递给便宜的对象,这些廉价对象在函数中应该是只读的,而当我们想要可以对其进行本地修改的副本时,传递非const值传递。
每个排列(按值/按引用和const /非const)都有重要的区别,这些区别绝对不是等效的。
当按值传递时,我们正在将数据复制到堆栈。如果我们为要传递的结构或者类定义了operator =,则将执行该运算符。我没有一个编译器指令,我知道这不会消除所提议的更改将固有地引起的隐式语言混淆的麻烦。
一种常见的最佳实践是通过const引用传递值,而不仅仅是通过引用传递值。这样可以确保在调用函数中不能更改该值。这是const正确代码库的一个元素。
完全const正确的代码库更进一步,将const添加到原型的末尾。考虑:
void Foo::PrintStats( void ) const { /* Cannot modify Foo member variables */ } void Foo::ChangeStats( void ) { /* Can modify foo member variables */ }
如果要将Foo对象传递给以const为前缀的函数,则可以调用PrintStats()。编译器在调用ChangeStats()时会出错。
void ManipulateFoo( const Foo &foo ) { foo.PrintStats(); // Works foo.ChangeStats(); // Oops; compile error }
我宁愿不要通过将每个(非限定)参数都设为引用来滥用引用。
将引用添加到C ++的主要原因是为了支持运算符重载。如果我们想使用"按引用传递"语义,C就有一种非常合理的实现方式:指针。
使用指针可以清楚地表明我们更改指针对象的意图,可以通过仅查看函数调用来查看此信息,而不必查看函数声明以查看其是否使用了引用。
另外,请参阅
I do want to change the argument, should I use a pointer or should I use a reference? I don't know a strong logical reason. If passing ``not an object'' (e.g. a null pointer) is acceptable, using a pointer makes sense. My personal style is to use a pointer when I want to modify an object because in some contexts that makes it easier to spot that a modification is possible.
来自相同的常见问题解答。
有一些不清楚的地方。当你说:
int b(b ¶m);
我们对第二个" b"有何打算?你忘了介绍一种类型吗?我们是否忘了对第一个" b"写不同的文字?你不认为写这样的东西很明显吗:
class B{/*something...*/}; int b(B& param);
从现在开始,我想你的意思是我写的。
现在,问题是"我们认为编译器将非POD的每个传递值都视为传递引用会更好吗?"。
第一个问题是它将破坏合同。我想你的意思是通过CONST引用,而不仅仅是引用。
现在,问题简化为:"我们是否知道是否有一些编译器指令可以按值优化函数调用?"
现在的答案是"我不知道"。
我想我们可能错过了C ++和C ++语义的要点。我们错过了C ++在按值传递(几乎)所有值方面都是正确的事实,因为这是在C.Always中完成的方式。但不仅在C语言中,我将在下面向我们展示...
C上的参数语义
在C语言中,一切都是通过值传递的。通过复制它们的值来传递"原始"和" POD"。在函数中对其进行修改,而原始文件将不会被修改。尽管如此,复制某些POD的成本还是很重要的。
使用指针表示法(*)时,不会通过引用传递。我们正在传递地址的副本。大致相同,但有一个细微的差别:
typedef struct { int value ; } P ; /* p is a pointer to P */ void doSomethingElse(P * p) { p->value = 32 ; p = malloc(sizeof(P)) ; /* Don't bother with the leak */ p->value = 45 ; } void doSomething() { P * p = malloc(sizeof(P)) ; p->value = 25 ; doSomethingElse(p) ; int i = p->value ; /* Value of p ? 25 ? 32 ? 42 ? */ }
p-> value的最终值为32. 因为p是通过复制地址的值传递的。因此,原始p未被修改(新的p被泄漏)。
Java和C Sharp上的参数语义
对于某些人来说可能令人惊讶,但是在Java中,所有内容也都按值复制。上面的C示例在Java中将给出完全相同的结果。这几乎是我们想要的,但是我们将无法像在C语言中那样轻松地通过"引用/指针"传递原始数据。
在C#中,他们添加了" ref"关键字。它的工作方式或者多或者少类似于C ++中的参考。关键是,在C#上,我们必须在函数声明以及每个调用中都提及它。我想这又不是我们想要的。
C ++上的参数语义
在C ++中,几乎所有内容都是通过复制值来传递的。当我们只使用符号的类型时,我们就是在复制符号(就像在C语言中一样)。这就是为什么当我们使用*时,需要传递符号地址的副本。
但是,当我们使用&时,假定我们正在传递真实的对象(无论是struct,int,pointer还是其他对象):引用。
容易将其误认为是语法糖(即在幕后,它像指针一样工作,并且生成的代码与指针所用的代码相同)。但...
事实是,引用不仅仅是语法上的糖。
- 与指针不同,它授权对对象的操作就好像在堆栈上一样。
- 脱机指针与const关键字相关联时,它授权从一种类型向另一种类型的隐式提升(主要是通过构造函数)。
- 与指针不同,该符号不应为NULL /无效。
- 与"按副本"不同,我们并不是在浪费无用的时间来复制对象
- 与"按副本"不同,我们可以将其用作[out]参数
- 与"按副本"不同,我们可以在C ++中使用完整范围的OOP(即,将完整对象传递给等待接口的函数)。
因此,引用具有两全其美的优点。
让我们看一下C示例,但是doSomethingElse函数具有C ++变体:
struct P { int value ; } ; // p is a reference to a pointer to P void doSomethingElse(P * & p) { p->value = 32 ; p = (P *) malloc(sizeof(P)) ; // Don't bother with the leak p->value = 45 ; } void doSomething() { P * p = (P *) malloc(sizeof(P)) ; p->value = 25 ; doSomethingElse(p) ; int i = p->value ; // Value of p ? 25 ? 32 ? 42 ? }
结果为42,旧的p被泄漏,由新的p代替。因为与C代码不同,我们不是传递指针的副本,而是传递指针的引用,即指针本身。
使用C ++时,上面的示例必须清晰易懂。如果不是,那么我们就缺少了一些东西。
总结
C ++是传递副本/值的方法,因为它是一切工作的方式,无论是在C中,在Java中是在Cor中(甚至在JavaScript中……:-p ...)。和C#一样,C ++具有引用运算符/关键字作为奖励。
现在,据我了解,我们也许正在半开玩笑地称之为C +,即具有某些有限的C ++功能的C。
也许解决方案使用的是typedefs(但是,如果看到无用的typedef污染了代码,这会激怒C ++同事……),但是这样做只会掩盖我们实际上确实缺少C ++的事实。
如另一篇文章中所述,我们应该将思维方式从C开发(任何形式)更改为C ++开发,或者我们可能应该转向另一种语言。但是请不要继续使用C ++功能对C方式进行编程,因为有意识地忽略/混淆我们所使用的习惯用法的力量,我们将产生次优的代码。
注意:除原始外,请勿传递任何副本。我们将从函数的OO能力中cast割函数,而在C ++中,这不是我们想要的。
编辑
该问题已进行了一些修改(请参阅https://stackoverflow.com/revisions/146271/list)。我让我原来的答案,并回答下面的新问题。
我们如何看待C ++上的默认按引用传递语义?就像我们说的那样,这将破坏兼容性,并且我们将对基元(即内置类型,仍将通过副本传递)和结构/对象(将作为引用传递)具有不同的传递。我们将不得不添加另一个运算符来表示"按值传递"(外部" C"非常糟糕,并且已经用于其他完全不同的东西)。不,我真的很喜欢今天用C ++完成的方式。
在我看来,引用运算符似乎是一种获取变量地址的方法,这就是我用来获取指针的方法。我的意思是,它是同一个运算符,但在不同的上下文中具有不同的语义,这对我们来说也感觉有点不对吗?是的,没有。当与C ++流一起使用时,运算符>>也更改了其语义。然后,我们可以使用运算符+ =替换strcat。我猜运算符&之所以被使用是因为它的含义是"与指针相反",并且因为他们不想使用另一个符号(ASCII受限制,并且范围运算符::以及指针->显示的是其他几个符号可用)。但是现在,如果&困扰我们,&&真的会让我们不安,因为他们在C ++ 0x中添加了一元&&(一种超级引用...)。我还没有自己消化呢...
我认为,如果我们开始混合所有可用参数及其const变体,则c ++会变得非常混乱。
迅速处理所有副本构造函数调用,所有取消引用重载等等。