如果返回结构体类型变量(named return value optimisation,NRVO)
貌似这是一个非常愚蠢的问题,因为对于具有良好素质的程序员而言,在C中函数返回类型为结构体类型是不是有点不合格,干嘛不用指针做传入传出呢?
测试环境:Linux IOS 3.2.0-45-generic-pae #70-Ubuntu SMP Wed May 29 20:31:05 UTC 2013 i686 i686 i386 GNU/Linux
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
此处谈下如果在C函数返回类型为大的结构体类型:
C++中严格区分初始化和赋值,但是C中没有区分初始化和赋值。
1 //该程序引述自:http://bbs.chinaunix.net/forum.php?mod=viewthread&action=printable&tid=1651248 2 //此链接中也有关于此文的讨论 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 7 8 const char *str = "Hello World\n"; 9 typedef struct 10 { 11 int m_Member1; 12 int m_Member2; 13 char m_String[20]; 14 }FUNCTION_STRUCT; 15 16 FUNCTION_STRUCT ReturnStruct(void) 17 { 18 FUNCTION_STRUCT internalData; 19 internalData.m_Member1 = 1; 20 internalData.m_Member2 = 2; 21 strcpy(&(internalData.m_String[0]), str); 22 23 return internalData; 24 } 25 26 int main(void) 27 { 28 FUNCTION_STRUCT externalData; 29 externalData = ReturnStruct(); 30 31 int a = externalData.m_Member1; 32 int b = externalData.m_Member2; 33 int c = a + b; 34 #if 0 35 printf("%d, %d, %s", 36 externalData.m_Member1, 37 externalData.m_Member2, 38 &externalData.m_String[0]); 39 #endif 40 41 return 0; 42 }
看下29行,实际上无论对于C或者C++(以文中开始处的测试环境为依据),ReturnStruct()都是有一个隐含的参数,其数据类型就是FUNCTION_STRUCT*,其存储空间在caller的栈中。
对C:
ReturnStruct中internalData在ReturnStruct函数的栈空间,当其执行到“return internalData"之前,会把internalData中的数据一个一个的拷贝到隐含参数所指向的空间中。那么开始传入的隐含参数与externalData的地址空间是否相同呢?答:当为”#if 0“时隐含参数与externalData的地址空间相同,故此时只有”一次“生成internalData+”一次“拷贝到externalData(编译器完成),当为“#if 1"时,隐含参数与externalData的地址空间不同,因此当从ReturnStruct中返回时,在caller中由编译器插入一些操作,将隐含参数指向的空间拷贝到externalData的地址空间,故此时只有”一次“生成internalData+"两次”拷贝(编译器完成);如果我们用指针作为传入传出参数,对C而言,效率可以大大提高,因为只需”一次“赋值到externalData。对于C而言,如果函数返回大的结构提类型,将callee中的栈帧的相应值拷贝到caller中的临时参数的地址空间是不可避免的,可能的区别就在于:在caller中是否要将临时参数所所指的地址空间的数据拷贝到目标空间(临时参数所指的地址空间与目标空间相同,则不用拷贝)。
所以对于C函数而言,如果写出的函数返回大的结构体数据类型,真的可以说不是一名合格的程序员(感觉返回小的结构体数据类型也不好啊,当然对于mips而言,返回8字节空间大小的结构体数据类型而言,直接用寄存器就可以了),即使出于可读性而言也不应该如此设计。
问题在于C++中,要是有程序员如此设计,我还真的不知该如何评价,因为当我们在函数中返回一个类类型对象时,有时既可以与显式的使用指针设计的函数效率相同,而且可读性也大大加强。感觉这与C++中严格区分初始化和赋值有关(我不确定)。
1 When certain criteria are met, an implementation is allowed to omit the copy construction of a class object, 2 even if the copy constructor and/or destructor for the object have side effects. In such cases, the implementation 3 treats the source and target of the omitted copy operation as simply two different ways of referring to 4 the same object, and the destruction of that object occurs at the later of the times when the two objects 5 would have been destroyed without the optimization.111) This elision of copy operations is permitted in the 6 following circumstances (which may be combined to eliminate multiple copies): 7 — in a return statement in a function with a class return type, when the expression is the name of a 8 non-volatile automatic object with the same cv-unqualified type as the function return type, the copy 9 operation can be omitted by constructing the automatic object directly into the function’s return value 10 — when a temporary class object that has not been bound to a reference (12.2) would be copied to a class 11 object with the same cv-unqualified type, the copy operation can be omitted by constructing the temporary 12 object directly into the target of the omitted copy 13 14 [Example: 15 class Thing { 16 public: 17 Thing(); 18 ˜Thing(); 19 Thing(const Thing&); 20 }; 21 Thing f() { 22 Thing t; 23 return t; 24 } 25 Thing t2 = f(); 26 Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing: 27 the copying of the local automatic object t into the temporary object for the return value of function f() 28 and the copying of that temporary object into object t2. Effectively, the construction of the local object t 29 can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program 30 exit. —end example]
上面时引述链接中所涉及的一段说明,我不是太明白,附下如下一段代码和运行结果,可以说明上文中的内容
1 #include <iostream> 2 3 class BASE 4 { 5 private: 6 int val; 7 public: 8 BASE(void):val(5) 9 { 10 std::cout << "BASE constructor" << std::endl; 11 std::cout << "own address: " << this << std::endl; 12 } 13 14 BASE(const BASE& base) 15 { 16 std::cout << "BASE copy constructor" << std::endl; 17 std::cout << "parameter address: " << &base << std::endl; 18 std::cout << "own address: " << this << std::endl; 19 val = base.val; 20 } 21 22 BASE& operator= (const BASE& base) 23 { 24 std::cout << "BASE assignment" << std::endl; 25 std::cout << "parameter address: " << &base << std::endl; 26 std::cout << "own address: " << this << std::endl; 27 val = base.val; 28 return *this; 29 } 30 31 ~BASE(void) 32 { 33 std::cout << "BASE deconstructor" << std::endl; 34 std::cout << "own address: " << this << std::endl; 35 } 36 }; 37 38 BASE getBASE(void) 39 { 40 BASE base; 41 std::cout << "in getBASE base address: " << &base << std::endl; 42 return base; 43 } 44 45 int main(void) 46 { 47 BASE base_one = getBASE(); 48 std::cout << "***********" << std::endl; 49 BASE base_two; 50 std::cout << "***********" << std::endl; 51 base_two = getBASE(); 52 return 0; 53 }
1 BASE constructor 2 own address: 0xbfecb0d4 3 in getBASE base address: 0xbfecb0d4 4 *********** 5 BASE constructor 6 own address: 0xbfecb0d8 7 *********** 8 BASE constructor 9 own address: 0xbfecb0dc 10 in getBASE base address: 0xbfecb0dc 11 BASE assignment 12 parameter address: 0xbfecb0dc 13 own address: 0xbfecb0d8 14 BASE deconstructor 15 own address: 0xbfecb0dc 16 BASE deconstructor 17 own address: 0xbfecb0d8 18 BASE deconstructor 19 own address: 0xbfecb0d4
首先,该程序范例不好,我没写/找出好点的范例。
关键处在于:
“BASE base_one = getBASE();"
"1 BASE constructor
2 own address: 0xbfecb0d4
3 in getBASE base address: 0xbfecb0d4
4 ***********"
仅调用了一次构造,而且程序的可阅读性加强了。
注意g++的命令行参数 -fno-elide-constructors