首先,我们需要回到按值传递和按引用传递的含义。
对于Java和SML之类的语言,按值传递很简单(并且没有按引用传递),就像复制变量值一样,因为所有变量都是标量并且具有内置的复制语义:它们要么算作算术输入C ++或“引用”(具有不同名称和语法的指针)。
在C语言中,我们有标量和用户定义的类型:
- 标量具有复制的数字或抽象值(指针不是数字,它们具有抽象值)。
- 聚合类型复制了所有可能初始化的成员:
- 对于产品类型(数组和结构):递归地,复制结构和数组元素的所有成员(C函数语法无法直接按值传递数组,而仅是结构的数组成员,但这是一个细节)。
- 对于总和类型(联合):保留“活动成员”的值;显然,逐个成员复制不是按顺序进行的,因为并非所有成员都可以初始化。
在C ++中,用户定义的类型可以具有用户定义的副本语义,从而可以使用拥有其资源所有权和“深度复制”操作的对象来实现真正的“面向对象”编程。在这种情况下,复制操作实际上是对几乎可以执行任意操作的函数的调用。
对于使用C ++编译的C结构,“复制”仍定义为调用用户定义的复制操作(构造函数或赋值运算符),该操作由编译器隐式生成。这意味着C / C ++通用子程序的语义在C和C ++中是不同的:在C中复制整个聚合类型,在C ++中调用隐式生成的复制函数来复制每个成员;最终结果是,无论哪种情况,每个成员都被复制。
(我认为,复制联合内部的结构是一个例外。)
因此,对于类类型,创建新实例的唯一方法(外部联合副本)是通过构造函数(即使对于那些使用普通编译器生成的构造函数的实例)。
您不能通过一元运算符获取右值的地址,&
但这并不意味着不存在右值对象。和一个对象,根据定义,具有一个地址 ; 而且该地址甚至可以用语法构造来表示:类类型的对象只能由构造函数创建,并且具有this
指针;但对于琐碎的类型,没有用户编写的构造函数,因此在this
构造和命名副本之前,没有放置任何内容的地方。
对于标量类型,对象的值是对象的右值,即存储在对象中的纯数学值。
对于类类型,对象值的唯一概念是该对象的另一个副本,该副本只能由副本构造函数(一个实函数)创建(尽管对于琐碎类型,该函数是如此特别琐碎,有时它们可以是创建而无需调用构造函数)。这意味着对象的值是通过执行更改全局程序状态的结果。它在数学上无法访问。
因此,按值传递确实不是一回事:它是通过拷贝构造函数call传递的,它不太漂亮。复制构造函数应根据对象类型的正确语义执行合理的“复制”操作,同时要尊重其内部不变式(即抽象用户属性,而不是固有的C ++属性)。
按类对象的值传递意味着:
- 创建另一个实例
- 然后使被调用的函数对该实例起作用。
请注意,此问题与副本本身是否是具有地址的对象无关:所有功能参数都是对象并且具有地址(在语言语义级别上)。
问题是:
- 副本是一个新对象,使用标量标量初始化了原始对象的纯数学值(真正的纯rvalue);
- 或副本是原始对象(与类一样)的值。
对于普通的类类型,您仍然可以定义原始成员副本的成员,因此由于复制操作(复制构造函数和赋值)的琐碎性,您可以定义原始成员的纯右值。对于任意的特殊用户功能而言并非如此:原始值必须是构造的副本。
类对象必须由调用方构造。构造函数形式上有一个this
指针,但形式主义在这里不相关:所有对象形式上都有一个地址,但只有那些实际上以非纯粹本地方式使用其地址的对象(与*&i = 1;
纯粹是本地使用地址的方式不同)才需要定义得很好地址。
如果对象在这两个单独编译的函数中都必须具有地址,则必须绝对按地址传递:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
这里即使something(address)
是纯函数或宏或任何(类似printf("%p",arg)
)不能保存地址或传达给另一个实体,我们有因为地址必须为唯一对象加以明确界定,按地址传递的要求int
有一个独特的身份。
我们不知道外部函数在传递给它的地址方面是否会是“纯”的。
在这里,在调用方的非平凡构造函数或析构函数中实际使用地址的可能性可能是采用安全,简单的路由并在调用方中赋予对象标识并传递其地址的原因,因为它使确保在构造函数中,构造之后和在析构函数中对其地址的任何非平凡使用都是一致的:this
在对象存在时,看起来必须相同。
像任何其他函数一样,非平凡的构造函数或析构函数也可以this
以要求其值保持一致性的方式使用指针,即使某些具有非平凡内容的对象可能不会:
struct file_handler { // don't use that class!
file_handler () { this->fileno = -1; }
file_handler (int f) { this->fileno = f; }
file_handler (const file_handler& rhs) {
if (this->fileno != -1)
this->fileno = dup(rhs.fileno);
else
this->fileno = -1;
}
~file_handler () {
if (this->fileno != -1)
close(this->fileno);
}
file_handler &operator= (const file_handler& rhs);
};
请注意,在那种情况下,尽管显式使用了指针(显式语法this->
),但对象标识无关紧要:编译器可以很好地使用按位复制对象来移动对象并进行“复制省略”。这基于this
特殊成员函数中使用的“纯度”级别(地址不会转义)。
但是纯度不是一个属性可在标准声明水平(编译器扩展存在于非内联函数声明添加纯度描述),所以基于对代码纯度可能不可用不能定义一个ABI(代码可以是或可能不是内联且可用于分析)。
纯度以“肯定纯”或“不纯或未知”来衡量。语义的共同点或上限(实际上是最大值)或LCM(最小公倍数)是“未知”。因此,ABI解决了未知问题。
摘要:
- 一些构造要求编译器定义对象标识。
- ABI是根据程序的类别而不是可以优化的特定情况定义的。
未来可能的工作:
纯度注释是否有用,足以被概括和标准化?