侧边栏壁纸
博主头像
noerror

虚灵不寐,众理具而万事出。

  • 累计撰写 239 篇文章
  • 累计创建 9 个标签
  • 累计收到 2 条评论
标签搜索

目 录CONTENT

文章目录

右值引用与返回值优化

noerror
2022-09-17 / 0 评论 / 0 点赞 / 447 阅读 / 1,082 字 / 正在检测是否收录...

对象的拷贝与移动

返回值优化

如果没有编译器的返回值优化,我们在设计函数时,返回一个对象并使用,往往带来多余的拷贝,影响程序的性能。例如设计一个函数从文件中读取大量的点信息:

std::vector<Point> getPointsFromFile(std::ifstream file) {
	std::vector<Point> result;
	while(!file.eof()) {
 		Point &pos = result.emplace_back();
 		file >> pos.x >> pos.y; // 这里只为了说明性能问题,不考虑代码安全
	}
	return result;
}

// 使用者上下文
std::vector<Point> points = getPointsFromFile(file);
process(points);

在没有编译器返回值优化的情况下,例如使用如下g++命令编译:

g++ ./main.cpp -fno-elide-constructors

result被构造填充,然后被拷贝到返回值临时对象,再由返回值临时对象拷贝到points。发生了两次拷贝,如果文件中存储着大量的点,拷贝操作将非常耗时。

如果将-fno-elide-constructors选项去掉,g++默认开启返回值优化:

g++ ./main.cpp

那么points和result实际是同一个对象,不存在任何拷贝。返回值优化的编译技术已经很成熟,目前常用版本的编译器即使使用-O0的默认优化选项,返回值优化也是默认打开的。

然而编译器的返回值优化并不是万能的,例如下面的场景:

std::vector<Point> getPoints(std::ifstream file) {
	std::vector<Point> leftAreaPoints;
	std::vector<Point> rightAreaPoints;
	return isLeft ? leftAreaPoints : rightAreaPoints
}

由于具体使用哪一个对象返回需要到运行时才能确定,编译器无法进行优化。如何才能设计一个合适的类,既能功能正常,又能在对象拷贝或移动时性能最佳,请接着看浅拷贝与深拷贝

浅拷贝与深拷贝

如果一个类有指针成员变量,就需要特别小心得对待拷贝构造函数。例如下面的代码:

class HasPtrMem {
public:
    HasPtrMem(): m_data(new int(0) {}
    ~HasPtrMem() { delete m_data; }
    int *m_data;
}

int main() {
    HasPtrMem a;
    HasPtrMem b = a; // 使用编译器默认生成的拷贝构造函数
    cout << *a.m_data << endl;
    cout << *b.m_data << endl;
} // 因重复释放m_data而崩溃

因为HasPtrMem类没有自定义拷贝构造函数,拷贝构造新对象时会使用编译器默认生成的拷贝构造函数。默认的拷贝构造函数只会将m_data的地址拷贝给新的对象,就像这样:

HasPtrMem(const HasPtrMem& other) {
	m_data = other.m_data;
}

当离开main函数时,对象a和b分别进行析构,m_data地址处的内存会被释放两次,从而导致程序崩溃。这种拷贝方式就是浅拷贝,问题的场景也是c++编程中经典的问题。最佳的解决方案是使用深拷贝,代码实现如下:

HasPtrMem(const HasPtrMem& other) {
	m_data = new int(*other.m_data);
}

这样对象间都有申请自己独立的内存空间,避免了不同对象操作相同内存带来的问题。然而大块内存的申请释放往往很耗时,对于一些特殊的场景可以进一步优化来提升性能,请接着看对象的移动Copy On Write

对象的移动

如果关闭了返回值优化,有这样一段代码:

HasPtrMem getTemp() {
    return HasPtrMem();
}

int main() {
    HasPtrMem a = getTemp();
}

getTemp函数中的HasPtrMem对象被构造出来后,先被拷贝到返回值临时变量,返回值临时变量再被拷贝到变量a。这里的两次拷贝,伴随着内存的申请、拷贝、释放,消耗大量的性能,相较于“拷贝”,我们更需要“剪切”。

在c++11引入了移动语义,对于将“临时变量”赋值给新变量的场景,可以出发移动构造函数,进而完成“剪切”操作。这里的“临时变量”有了一个专业而完整的定义:右值

Copy On Write

0

评论区