C++类对象的隐式类型转换和编译器返回值优化

前言

在类与对象的学习过程中,一定会对隐式类型转换这个词不陌生。对于内置类型而言,相似的类型会支持隐式类型转换,例如int a = 3.1;在这篇文章我们细细谈谈类对象中的隐式类型转换

  1. 类对象的隐式类型转换
  2. 编译器的优化

注意:这里的讨论,没有考虑右值

1. 隐式类型转换

关于隐式类型转换产生临时变量,我在类型转换细节中有谈到,大家可以先阅读一下。

1.1 单参数的隐式类型转换

C++11之前,C++98仅仅支持单参数的隐式类型转换,当然这种转换也带来了很多的便利!同时也有潜在危险,需要合理看待!

为了方便测试,我们在VS2022下,给出以下的A类:

#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 1)
		:_a(a)
	{
		cout << "A(int a = 1)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}
	//暂时不考虑移动构造和移动赋值
	int _a;
};

在大多数时候,我们会写出这样的代码, 例1.1.1:

int main()
{
	A a = 1; //直接以1来赋值这个a类对象
	return 0;
}

这个过程会发生什么呢?

  1. 编译器会先利用1来构造一个A类的tmp对象

  2. 调用a的对象的拷贝构造函数

(但似乎这样的代价也太大了,所以编译器会做出优化,我们稍后再谈)

当我们在使用STL的时候,也常常会发生这种隐式类型的转换,例如:

#include<iostream>
#include<string>
#include<vector>
using namespace std;

int main()
{
	vector<string> v;
	v.push_back("this is a string?");
	return 0;
}

我们的vector存放的是string类对象,而我们传入的却是一个char*类型(字符串常量类型都被解释为了const char *)。在不考虑其它因素外,这个时候就应该会发生隐式类型的转换:将char*类型构造一个string对象,再调用push_back函数

这就是单参数的隐式类型转换

1.2 多参数的隐式类型转换

C++11支持了列表初始化
在这里插入图片描述
(C++11我们会在后面的专题谈到)

也对类的多参数的情况进行了升级!支持了多参数的隐式类型转换。给出下面一个例子:

#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{
		cout << "A(int a = 1, int b = 2)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}
	int _a;
	int _b;
};

那么支持了多参数的隐式类型转换之后,我们可以这么写
例1.2.1:

int main()
{
	A a = {1, 3}; //支持的
	return 0;
}

同样地,我们应该了解到这个语句干了什么?

  1. 编译器会先利用{1, 3}来构造一个A类的tmp对象

  2. 调用a的对象的拷贝构造函数

1.3 explicit关键字

有些时候,我们其实并不想构造函数支持这种隐式类型转换,我们就可以采用关键字explicit来对构造函数进行声明!

语法如下:

class A
{
public:
	explicit A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{
		cout << "A(int a = 1, int b = 2)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	int _a;
	int _b;
};

int main()
{
	//关于这样的代码就无法通过编译了!
	//A a = {1, 3}; //不支持了
	return 0;
}

但是这样真的很方便……所以我们还可以采用另外一个形式,匿名对象

例1.3.1

int main()
{
	A a = A{ 1, 3 }; //语句一
	//A a = A({ 1, 3 }); //语句二
	return 0;
}

说明:

  1. 语句一可以在支持隐式类型转换的情况下使用。本质和隐式类型转换类似,都是构造一个tmp类对象。
  2. 语句二使用的前提是这个A类支持initializer_listA类构造函数。本质上是{ }调用了initializer_list的构造函数,是一个initializer_listtmp对象,然后再初始化A类。所以,当你的A类不支持这样的一个构造函数,就无法成功初始化了!

是否需要验证呢?

#include<iostream>
using namespace std;
class A
{
public:
	explicit A(int a = 1, int b = 2)
		:_a(a)
		, _b(b)
	{
		cout << "A(int a = 1, int b = 2)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	A(initializer_list<int> il) //支持列表初始化的构造函数
	{
		cout << "A(initializer_list<int> il)" << endl;
	}
	int _a;
	int _b;
};

int main()
{
	A a = A({ 1, 3 }); //注意不要写成这样
	return 0;
}

在这里插入图片描述

这样的调用,不知道是否有说服力呢?

2. 编译器的优化

在上面的大多数例子中,我并没有验证那些我们看起来的步骤。因为:编译器是会对同一行的连续构造采取优化措施的

现在,在来考虑这个类,和几条语句:

2.1 普通构造优化

例2.1.1:

class A
{
public:
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{
		cout << "A(int a = 1, int b = 2)  " << _a << endl; //为了区别每一个构造,这里多给了一个打印
	}
	~A()
	{}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}
	int _a;
	int _b;
};

int main()
{
	A a0(-1, -1);
	cout << " ------------------ " << endl;
	A a1 = a0; //语句一
	A a2 = { 0, 0 }; //语句二
	A a3 = A{ 1, 1 }; //语句三
	return 0;
}

来看运行结果:
在这里插入图片描述
(单参数的也是这样的结果)
在此之前,我们并没有给出实际的运行结果,因为编译器会为我们做出优化:

  • 语句一没有优化。a0本身就是一个存在的对象!
  • 语句二、三进行了优化,本来我们应该是先普通构造再拷贝构造,但编译器为我们直接构造

2.2 函数传参优化

同样直接给出示例:

class A
{
public:
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{
		cout << "A(int a = 1, int b = 2)  " << _a << endl; //为了区别每一个构造,这里多给了一个打印
	}
	~A()
	{}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}
	int _a;
	int _b;
};

void func(A a) //注意这里并没有传引用 -- 传引用就不会进行拷贝构造了
{
	//……
}

int main()
{
	A a0(-1, -1);
	cout << " ------------------ " << endl;
	func(a0); //语句一
	func({ 2, 2 }); //语句二
	func(A{ 3, 3 }); //语句三
	return 0;
}

在这里插入图片描述

同样发生了优化!

2.3 函数返回优化

这里的函数返回值优化又有所不同,返回值优化又被称为:RVO(Return Value Optimization)

给出示例:

class A
{
public:
	A(int a = 1, int b = 2)
		:_a(a)
		,_b(b)
	{
		cout << "A(int a = 1, int b = 2)  " << _a << endl; //为了区别每一个构造,这里多给了一个打印
	}
	~A()
	{}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
	A& operator=(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}
	int _a;
	int _b;
};

A func()
{
	A tmp(5,5);
	//……
	return tmp;
}

int main()
{
	A a0(-1, -1);
	cout << " ------------------ " << endl;
	a0 = func(); //语句一

	A a1 = func(); //语句二
	return 0;
}

我们在func中创建一个临时变量tmp,想让这个tmp完成一些业务,然后返回这个临时变量。来看运行结果:
在这里插入图片描述
语句一:创建一个tmp对象,然后调用一个赋值运算符重载,十分合理的。但是看到语句二直接就完成了构造!。没有在func()中调用tmp的构造函数?还是没有调用a1的构造函数?

  1. 首先分析:A a1 = func();。首先函数返回的时候是会将返回值拷贝到一个tmp对象中的,然后再通过这个tmp对象返回给外面的接收变量,这里本身就有两个拷贝构造,编译器发生优化是很情理之中的!
  2. 同时函数func中又定义了一个变量,这个变量也会调用一个构造函数的。
  3. 可是结果告诉我们整个的调用只调用了一次构造函数

没错,这就是编译器的RVO

RVO编译器优化技术。它可以减少函数返回时创建临时对象的次数,从而提高程序的运行效率。RVO主要针对未命名的临时对象,消除了函数返回时创建的临时对象,避免了不必要的拷贝构造函数调用。

来看这样一张图片:
在这里插入图片描述

发现了吗?tmp这个对象被处理成为了一个指针!这个指针指向的对象就是a1对象。我们通过对tmp的操作,在编译器看来就是对a1进行操作。所以上述情况下只会调用一次拷贝构造函数

在有些时候,这样采用RVO的代码效率不差同时也更好维护!大家可以自己做性能测试!

RVO并不总是适用,存在一些限制条件,例如:

  • 函数抛出异常时,RVO可能不会进行。

  • 函数可能返回具有不同变量名的对象时,RVO无法进行。

  • 函数有多个出口时,RVO可能不会进行。

希望这篇文章能够帮助到你!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值