程序转换语意

先看下面示例代码:

class CTest
{
public:
	CTest()
	{
		cout << "CTest()" << endl;

		m_nId = 10;
	}
	CTest(const CTest& objTest)
	{
		cout << "CTest(const CTest& objTest)" << endl;
		m_nId = 200;
	}
	void PrintData()
	{
		cout << m_nId << endl;
	}
private:
	int m_nId;
};

CTest DoSomethingA()
{
	CTest objTest;

	return objTest;
}

CTest DoSomethingB()
{
	return CTest();
}

void DoSomethingC(CTest objTest)
{
	objTest.PrintData();
}

显式的初始化


CTest objTest;
{
	CTest t1(objTest);
	CTest t2 = objTest;
	CTest t3 = CTest(objTest);
}

对于这样的调用输出:

CTest()
CTest(const CTest& objTest)
CTest(const CTest& objTest)
CTest(const CTest& objTest)

这里涉及到双阶段转换

  1. 重写调用的每个定义,其中初始化操作被剥除

  2. class CTest的copy constructor调用操作会被安插进去

VS编译器可能会做这样的转换:

{
	// 初始化操作被剥除
	CTest t1;
	CTest t2;
	CTest t3;

	// 调用拷贝构造
	t1.CTest::CTest(objTest)
	t2.CTest::CTest(objTest)
	t3.CTest::CTest(objTest)
}

参数的初始化


{
	CTest objTest;
	DoSomethingC(objTest);
}

输出:

CTest()
CTest(const CTest& objTest)
200

C++ standard说,把一个class object当作参数传给一个函数或者是作为函数的返回值相当于这样的转换CTest objTest = arg;,编译器实现技术上有一种策略是导入所谓的临时对象,并调用copy constructor将它初始化,然后将临时对象以引用的方式交给函数, 并在函数返回前调用destructor(如果有),所以上面的调用可能被编译器转换成类似下面这样:

CTest tmpObject;
// 调用拷贝构造
tmpObject.CTest.CTest(objTest)

void DoSomethingC(CTest& tmp)
DoSomethingC(tmpObject)

返回值初始化


像上面代码示例提到的一个函数:

CTest DoSomethingA()
{
	CTest objTest;

	return objTest;
}

对于这个函数的调用:

{
	CTest objTestA = DoSomethingA();
}

输出:

CTest()
CTest(const CTest& objTest)

这里涉及到一个编译器的另一种实现方式“拷贝建构(copy construct)”,它的方式是把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。上述调用分为两个阶段转化:

  1. 对函数DoSomethingA加上额外参数,类型是CTest object 的引用。这个参数将用来放置“拷贝建构(copy construct)”而得的返回值

  2. 在return之前安插copy constructor调用操作,以便将欲传回的object的内容当作上述新增参数的初值

对应编译器转化后代码可能是这样:

void DoSomethingA(CTest& objTest)
{
	CTest tmpTest;
	tmpTest.CTest::CTest();

	objTest.CTest::CTest(tmpTest);
	return;
}

对于程序中调用DoSomethingA的地方可能做出的转化:

{
	// 初始化操作被剥除
	CTest objTestA;
	DoSomethingA(objTestA);
}

使用者层面的优化


上面的代码示例提到的这个函数:

CTest DoSomethingB()
{
	return CTest();
}

对应编译器转化后代码可能是这样:

void DoSomethingB(CTest& objTest)
{
	objTest.CTest::CTest();
	return;
}

调用原型:

{
	CTest objTestB = DoSomethingB();
}

编译器转化后:

{
	// 初始化操作被剥除
	CTest objTestB;
	DoSomethingB(objTestB);
}

对应这样的转化结果,也就可以解释了调用DoSomethingB输出的结果:

CTest()

编译器优化


对于上面提到的代码示例编译器优化的程度视不同的编译器而定,上面提到的对copy constructor的调用要视不同编译器而定,可以在目标编译器上 运行后才能知道对copy constructor的调用是不是你所期望的。例如有些时候你可能就希望调用copy constructor。这时就要做相应的测试才能得知。

关于编译器的优化不做讨论,就像上面的代码示例:

CTest DoSomethingA()
{
	CTest objTest;

	return objTest;
}

被称为NRV优化技术,而编译器对于NRV技术是否支持和什么时候使用NRV技术等等以后讨论

要不要copy constructor


对于我们上面提到的代码示例,没有必要提供copy constructor,而copy constructor也被编译器视为trivial(关于copy constructor的 copy constructor的介绍可以参考这篇文章),当发生copy时会触发高效的”bitwise copy”操作,而需要大量的memberwise copy时, 那么提供显式的copy constructor是有必要的(不只有这一个场景)。对于显式的copy constructor里执行copy的手段也有不同,可以逐一赋值、可以借助memset、memcpy等等,当使用memset、memcpy时有可能破坏编译器产生的内部members,例如class声明一个或多个virtual functions,或者内含virtual base class,那么使用memset、memcpy时都会破坏编译器产生的内部members初值

最后


copy constructor 的应用迫使编译器或多或少对程序代码做转化,从上面的例子,发现尤其当一个函数以by value的方式传回class object,不论这个class显示定义copy constructor还是合成的,都会导致转化,具体的转化还要针对具体的编译器测试而定。多一些测试 对于何时调用default constructor、何时调用copy constructor做到心中有数,写出的代码才可以按照我们预想的逻辑运行