2.3 程序转化语意学

#include "X.h"
X foo() {
X xx;
return xx;
}

一个人可能会对代码做出以下假设:

  1. 每次 foo()被调用,就传回 xx的值。
  2. 如果 class X定义了一个 copy constructor,那么当 foo()被调用时,保证该copy constructor也会被调用。

然而

1、由X的定义决定。

2、部分地取决于X的定义,最主要还是取决于c++编译器的优化层级有多高。

在一个高质量的c++编译器中,以上两点假设对于class X的nontrivial definitions都是错误的。

2.3.1 显式的初始化操作(Explicit Initialization)

已知有这样的定义:

X x0;
//下面有三个定义,每一个都明显地以x0来初始化其 class object:
void foo_bar() {
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}

必要的程序转化有两个阶段:

  1. 重写每一个定义,其中的初始化操作会被剥除。
  2. class 的copy constructor调用操作会被插入。

例如,在显式的双阶段转化之后,foo_bar()可能看起来像这样:

//    可能的程序转换
// C++伪码
void foo_bar() {
X x1; //定义被重写,初始化操作被剥除
X x2; //定义被重写,初始化操作被剥除
X x3; //定义被重写,初始化操作被剥除
// 编译器插入X copy constructor的调用操作
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
}

其中的:

x1.X::X(x0);

就表现出对以下的copy constructor的调用:

X::X(const X &xx);

2.3.2 参数的初始化(Argument Initialization)

C++ Standard说, 把一个 class object当做参数传给一个函数(或者是作为一个函数的返回值),相当于以下形式的初始化操作:

X xx = arg;

其中 xx代表形式参数(或返回值),而arg代表真正的参数值(实际参数值)。因此,若已知这个函数:

void foo(X x0);

下面这样的调用方式:

X xx;
//...
foo(xx);

将会要求局部实例(local instance)x0以memberwise的方式将xx当做初值。在编译器实现技术上,有一种策略是导入所谓的临时性 object,并调用 copyconstructor将它初始化,然后将此临时性object交给函数。例如将前一段程序代码转换如下:


// C++伪码
// 编译器产生出来的临时对象
X __temp0;
// 编译器对copy constructor的调用
__temp0.X::X(xx);
// 重新改写函数调用操作,以便使用上述的临时对象
foo(__temp0);

然而这样的转换只做了一半功夫而已,问题出在foo()的声明上。临时性object先以class X的copy constructor正确地设定了初值,然后再以bitwise方式拷贝到x0这个局部实例中。foo()的声明因而也必须被转化,形式参数必须从原先的一个 class X object 改变为一个 class X的引用(reference),像这样:

void foo(X&x0);

其中class X声明了一个destructor,它会在foo()函数完成之后被调用,销毁那个临时性的object。

另一种实现方法是以“拷贝建构”(copy construct)的方式把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。在函数返回之前,局部对象(local object)的destructor(如果有定义的话)会被执行。Borland C++编译器就是使用此法,但它也提供一个编译选项,用以指定前一种做法,以便和其早期版本兼容。

2.3.3 参数的初始化(Argument Initialization)

已知下面这个函数定义

X bar() {
X xx;
// 处理xx...
return xx;
}

你可能会问bar()的返回值如何从局部对象xx中拷贝过来?Stroustrup在cfront中的解决做法是一个双阶段转化:

  1. 首先加上一个额外参数,类型是 class object的一个 reference。这个参数将用来放置被“拷贝建构(copy constructed)”而得的返回值。
  2. 在 return指令之前安插一个 copy constructor调用操作,以便将欲传回之object的内容当做上述新增参数的初值。

真正的返回值是什么?最后一个转化操作会重新改写函数,使它不传回任何值,根据这样的算法,bar()转换如下:

// 函数转换以反映出copy constructor的应用
// C++伪代码
void bar(X &__result) // 加上一个额外参数
{
X xx;
// 编译器所产生的default constructor调用操作
xx.X::X();
// ...处理xx
// 编译器所产生的copy constructor调用操作
__result.X::XX(xx);
return;
}

现在编译器必须转换每一个bar()调用操作,以反映其新定义。例如:

X xx = bar();

将被转换为下列两个指令句:

// 注意,不必施行default constructor
X xx;
bar(xx);

而:

bar().memfunc();

可能被转化为:

// 编译器所产生的暂时现象
X __temp0;
(bar(__temp0), __temp0).memfunc();

同样道理,如果程序声明了一个函数指针,例如:

X (*pf)();
pf = bar;

它必须被转化为:

void (*pf)(X &);
pf = bar;

2.2.4 在用户层级做优化(Optimization at the User Level)

Jonathan Shopiro他对于像bar()这样一个函数,最先提出“程序员优化”的观念:定义一个“计算用”的constructor。

换句话说程序员不再写:

X bar(const T &y, const T &z) {
X xx;
// ...以y和z来处理xx
return xx;
}

那会要求xx被"memberwise"地拷贝到编译器所产生的__result中,Jonathan定义另一个constructor,可以直接计算xx的值:

X bar(const T &y, const T &z) {
return X(y, z);
}

于是当bar()定义被转换之后,效率会比较高:

// C++伪代码
void bar(X &__result)
{
__result.X::X(y, z);
return;
}

__result被直接计算出来,而不是经由copy constructor拷贝得到。不过这种解决方法受到了某种批评,担心那些特殊计算用途的constructor可能会大量扩散。在这个层次上, class 的设计是以效率考虑居多,而不是以"支持抽象化"为优先。

2.3.5 在编译器层面做优化(Optimization at the Compiler Level)

  1. 在一个如bar()这样的函数中,所有的 return 指令返回相同的具名数值(named value),因此 编译器可能自己优化,方法是以result参数取代named return value。例如下面的:

bar()定义:

X bar() {
X xx;
// ...处理xx
return xx;
}

编译器把其中的xx用__result取代,直接处理__result:

void bar(X &__result) {
// default constructor 被调用
// C++伪代码
__result.X::X();
// ...直接处理__result
return;
}

这样的编译器优化操作,有时被称为Named Return Value(NRV)优化

  1. 但定义了copy constructor 才会激活C++编译器中的NRV优化。

如何理解?

NRV优化和拷贝构造函数是有关系的,只有定义了拷贝构造函数才会开启NRV优化,但现代编译器NRV优化的开启一般都与拷贝构造函数没有关系,早期的 cfront需要一个开关来决定是否应该对代码实行NRV优化,这就是是否有客户(程序员)显式提供的拷贝构造函数:如果客户没有显式地提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,所有没必要再对其实施NRV优化;但如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。

————————————————

版权声明:本文为CSDN博主「uestc_chenmo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/uestc_chenmo/article/details/80342719

虽然NRV优化提供了重要的效率改善,但它还是饱受批评。

其中一个原因是,优化由编译器默默完成,而它是否真的被完成,并不十分清楚(因为很少有编译器会说明其实现程度,或是否实现)

第二个原因是,一旦函数变得比较复杂,优化也就变得比较难以施行。

3.虽然下面三个初始化操作在语意上相等:

X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;

但是在第二行和第三行中,语法明显地提供了两个步骤的初始化操作:

  1. 将一个暂时性的object设以初值1024
  2. 将暂时性的object以拷贝建构的方式作为explicit object的初值

    换句话说,xx()是被单一的constructor操作设定初值:
// C++伪代码
xx0.X::X(1024);

而xx1或xx2却调用两个constructor,产生一个暂时性object,并针对该暂时性objet调用 class X的destructor:

// C++伪代码
X __temp0;
__temp0.X::X(1024);
xx1.X::X(__temp0);
__temp0.X::~X();

先后调用构造函数和拷贝构造函数,是对于编译器未进行优化的情况下而言。如今在g++编译器中,默认会进行拷贝优化。若关闭优化,编译时需要添加-fno-elide-constructors选项。

一般而言, 面对"以一个 class object作为另一个 class object的初值"的情况,语言允许编译器有大量的自由发挥空间,其好处是导致码产生时有明显的效率提升,缺点是不能够安全地规划copy constructor的副作用,必须视其执行而定。

2.3.6 Copy Constructor:要还是不要?

  1. 已知下面的3D坐标点类:
class Point3d {
public:
Point3d(float x, float y, float z);
private:
float _x, _y, _z;
};

这个class的设计者应该提供一个explicit copy constructor吗?

上述class的default copy constructor被视为trivial。它既没有任何member(或base)class objects 带有copy constructor,也没任何的virtual base class 或virtual function。所以,默认情况下,一个Point3d class object的“memberwise”初始化操作会导致“bitwise copy”。这样的效率很高,但安全吗?答案是yes。三个坐标成员是以数值来存储的。bitwise copy既不会导致memoryleak,也不会产生address aliasing。因此,它既快速,又安全。

  1. 那么,这个class的设计者应该提供一个explicit copy constructor吗?你将如何回答这个问题?答案当然很明显是no。没有任何理由要你提供一个copyconstructor函数实例,因为编译器自动为你实施了最好的行为。比较难以回答的是,如果你被问及是否预见class需要大量的memberwise初始化操作,例如以传值(by value)的方式传回objects?如果确实如此,那么提供一个copyconstructor的explicit inline函数实例就非常合理——在“你的编译器提供NRV优化”的前提下

例如, Point3d支持下面一组函数:

Point3d operator+(const Point3d &, const Point3d &);
Point3d operator-(const Point3d &, const Point3d &);
Point3d operator*(const Point3d &, int);

所有的那些函数都能够良好地符合NRV template:

{
Point3d result;
// 计算result
return result;
}

实现copy constructor的最简单方法如下所示:

Point3d::Point3d(const Point3d &rhs) {
_x = rhs._x;
_y = rhs._y;
_z = rhs._z;
}

这没问题,但使用C++ library的memcpy()会更有效率:

Point3d::Point3d(const Point3d &rhs) {
memcpy(this, &rhs, sizeof(Point3d));
}

然而不管使用 memcpy()还是 memset(),都只有在“classes 不含任何由编译器产生的内部members”时才能有效运行。如果Point3d class声明一个或一个以上的virtual functions,或内含一个virtual base class,那么使用上述函数将会导致那些“被编译器产生的内部members”的初值被改写。例如,已知下面的声明:

class Shape {
public:
// 这会改变内部的vptr
Shape() {
memset(this, 0, sizeof(Shape));
}
virtual ~Shape();
//...
};

编译器为此constructor扩张的内容看起来像这样:

// 扩张后的constructor
// C++伪代码
Shape::Shape() {
// vptr必须在使用者的代码执行之前先设定妥当
__vptr__Shape = __vtbl__Shape;
// memset会将vptr清为0
memset(this, 0, sizeof(Shape));
};

2.3.7 总结

copy constructor的应用,迫使编译器多多少少对你的程序代码做部分转化。尤其是当一个函数以传值(by value)的方式传回一个class object,而该class有一个copy constructor(不论是显式定义出来的,或是合成的)时。这将导致深奥的程序转化——不论在函数的定义上还是在使用上。此外,编译器也将 copyconstructor的调用操作优化,以一个额外的第一参数(数值被直接存放于其中)取代 NRV。程序员如果了解那些转换,以及copy constructor 优化后的可能状态,就比较能够控制其程序的执行效率。

The Semantics of Constructors——2.3 程序转化语意学的更多相关文章

  1. 如何将常规的web 应用程序转化为云上多租户 SaaS 解决方案

    如何将web 应用程序转化为多租户 SaaS 解决方案 https://www.ibm.com/developerworks/cn/cloud/library/cl-multitenantsaas/i ...

  2. web 应用程序转化为多租户 SaaS 解决方案

    web 应用程序转化为多租户 SaaS 解决方案 https://www.ibm.com/developerworks/cn/cloud/library/cl-multitenantsaas/inde ...

  3. 【Linux】进程的结构,创建,结束,以及程序转化为的进程的过程

    本文内容: 1.进程的结构 2.程序转化为进程的过程 3.进程的创建 4.进程的结束 背景知识: 1.进程是计算机中处于运行的程序的实体 2.进程是线程的容器 3.程序本身只是指令,数据以及组织形式的 ...

  4. C++对象模型(一):The Semantics of Constructors The Default Constructor (默认构造函数什么时候会被创建出来)

    本文是 Inside The C++ Object Model, Chapter 2的部分读书笔记. C++ Annotated Reference Manual中明确告诉我们: default co ...

  5. The Semantics of Constructors(拷贝构造函数之编译背后的行为)

    本文是 Inside The C++ Object Model's Chapter 2  的部分读书笔记. 有三种情况,需要拷贝构造函数: 1)object直接为另外一个object的初始值 2)ob ...

  6. The Semantics of Constructors: The Default Constructor (默认构造函数什么时候会被创建出来)

    本文是 Inside The C++ Object Model, Chapter 2的部分读书笔记. C++ Annotated Reference Manual中明确告诉我们: default co ...

  7. 【C++】深度探索C++对象模型读书笔记--构造函数语义学(The Semantics of constructors)(四)

    成员们的初始化队伍(member Initia 有四种情况必须使用member initialization list: 1. 当初始化一个reference member时: 2. 当初始化一个co ...

  8. [转]《深度探索C++对象模型》读书笔记[一]

    前 言 Stanley B.Lippman1.   任何对象模型都需要的三种转换风味: ü        与编译器息息相关的转换 ü        语言语义转换 ü        程序代码和对象模型的 ...

  9. 深入探索C++对象模型(二)

    构造函数语义学(The Semantics of Constructors) Default Constructor的构造操作 对于class X,如果没有任何user-declared constr ...

  10. c++学习书籍推荐《深度探索C++对象模型》下载

    百度云及其他网盘下载地址:点我 百度云及其他网盘下载地址:点我 编辑推荐 如果你是一位C++程序员,渴望对于底层知识获得一个完整的了解,那么这本<深度探索C++对象模型>正适合你 作者简介 ...

随机推荐

  1. CSS3 滑过加过渡效果

    .vogueHover { width: 100%;height: 100%; position: absolute; left: 0; top: 0; background: #001e50; z- ...

  2. Mybatis 实体类驼峰命名与数据库字段之间映射

    数据库的命名规则和 Java 的命名规则不一致,导致实体类与数据库字段不能完美映射. 一.可以在 mapper.xml 中通过 resultMap 来解决: <resultMap id=&quo ...

  3. java学习日记20230225-java介绍

    sun公司 oak语言--java gosling >sun 1995 java第一个版本 2009年 甲骨文公司收购sun 2011 java7 2014 java8 2022-2030年 长 ...

  4. 1137. 第 N 个泰波那契数 (Easy)

    问题描述 1137. 第 N 个泰波那契数 (Easy) 泰波那契序列 T 定义如下: T = 0, T = 1, T = 1, 且在 n >= 0 的条件下 T = T + T + T 给你整 ...

  5. RDD编程练习

    一.filter,map,flatmap练习: 1.读文本文件生成RDD lines 2.将一行一行的文本分割成单词 words 3.全部转换为小写 4.去掉长度小于3的单词 5.去掉停用词 6.练习 ...

  6. 使用clipboard.js复制文字+图片到微信后图片不显示问题处理

    使用clipboard.js复制文字 +图片,粘贴到微信不显示图片,而QQ可以. 解决方案:图片链接使用http,不要使用https. 使用clipboard.js实现复制功能 文字 +图片到微信客户 ...

  7. 使用batch-import工具向neo4j中导入海量数据【转】

    转载备忘 链接:https://www.yisu.com/zixun/496254.html 这篇文章给大家分享的是有关数据库中怎么使用batch-import工具向neo4j中导入海量数据的内容.小 ...

  8. mysql错误号码2003 can't connect to mysql server on 'localhost' (0)解决方案

    找到mysql安装目录下的Bin目录,在cmd中进入这个目录,运行net start mysql 却显示服务名无效 所以此时 首先在MYSQL的安装目录bin下,输入mysqld --install ...

  9. [jQuery]判断页面是否滚动到底部

    方法1:判断可见高度+滚动高度是否等于内容高度 但经过测试UC.QQ.华为浏览器,这个方法不生效.(打印查因:可能由于屏幕缩放,可见高度和滚动高度会偏小. $(this).scroll(functio ...

  10. Docker之Nginx保姆级别安装

    Docker之Nginx保姆级别安装: 如果觉得样式不好:跳转即可 http://www.lifengying.site/(md文件复制过来有些样式会不一样) 学英语网站项目:自己先保证Redis.N ...