用C或C++返回一个结构安全吗
Is it safe to return a struct in C or C++?
我所理解的是不应该这样做,但我相信我见过这样的例子(注意代码不一定语法正确,但想法是存在的)
typedef struct{
int a,b;
}mystruct;
然后这里有一个函数
mystruct func(int c, int d){
mystruct retval;
retval.a = c;
retval.b = d;
return retval;
}
我知道,如果我们想做这样的事情,我们应该总是返回一个指向malloc’ed结构的指针,但我确信我看到过这样的例子。这是正确的吗?就我个人而言,我总是返回一个指向malloc’ed结构的指针,或者只是通过引用传递函数并修改那里的值。(因为我的理解是,一旦函数的作用域结束,用于分配结构的任何堆栈都可以被覆盖)。
让我们为这个问题添加第二部分:这会因编译器而异吗?如果是,那么最新版本的桌面编译器(gcc、g++和Visual Studio)的行为是什么?
对这件事有什么看法?
它非常安全,这样做没有错。此外:它不会因编译器而异。
通常,当(像您的示例一样)您的结构不太大时,我认为这种方法甚至比返回malloc’ed结构更好(malloc
是一种昂贵的操作)。
非常安全。
您正在按价值返回。如果您通过引用返回,则会导致未定义的行为。
//safe
mystruct func(int c, int d){
mystruct retval;
retval.a = c;
retval.b = d;
return retval;
}
//undefined behavior
mystruct& func(int c, int d){
mystruct retval;
retval.a = c;
retval.b = d;
return retval;
}
您的代码片段的行为是完全有效和定义的。它不会因编译器而异没关系
就我个人而言,我总是返回一个指向malloc’ed结构的指针
你不应该。尽可能避免动态分配内存。
或者只是通过引用函数并修改值那里
此选项完全有效。这是一个选择的问题。通常,如果您想在修改原始结构的同时从函数返回其他内容,则可以执行此操作。
因为我的理解是,一旦此外,用于分配结构的任何堆栈都可以重写
这是错误的。我的意思是,这有点正确,但您返回了在函数中创建的结构的副本理论上。在实践中,RVO可能会发生。阅读回报值优化。这意味着,尽管retval
在函数结束时似乎超出了作用域,但它实际上可能是在调用上下文中构建的,以防止额外的复制。这是一个编译器可以自由实现的优化。
当您离开函数时,函数中mystruct
对象的生存期确实结束了。但是,您在return语句中按值传递对象。这意味着对象被从函数中复制到调用函数中。原始对象不见了,但副本仍然存在。
不仅可以安全地在C中返回struct
(或在C++中返回class
,其中struct
-s实际上是具有默认public:
成员的class
-es),而且很多软件都在这样做。
当然,在C++中返回class
时,该语言指定将调用一些析构函数或移动构造函数,但在许多情况下,编译器可以对其进行优化
此外,Linux x86-64 ABI规定,返回具有两个标量(例如指针或long
)值的struct
是通过寄存器(%rax
&%rdx
)完成的,因此非常快速高效。因此,对于这种特殊情况,返回这样两个标量字段struct
可能比执行任何其他操作(例如,将它们存储到作为参数传递的指针中)更快。
返回这样一个双标量字段struct
比malloc
返回它并返回指针快得多。
这是完全合法的,但对于大型结构,需要考虑两个因素:速度和堆栈大小。
我也同意sftrabbit,生命确实结束了,堆栈区域被清除了,但编译器足够聪明,可以确保所有数据都应该在寄存器或其他方式中检索。
下面给出了一个简单的确认示例。(取自Mingw编译器汇编)
_func:
push ebp
mov ebp, esp
sub esp, 16
mov eax, DWORD PTR [ebp+8]
mov DWORD PTR [ebp-8], eax
mov eax, DWORD PTR [ebp+12]
mov DWORD PTR [ebp-4], eax
mov eax, DWORD PTR [ebp-8]
mov edx, DWORD PTR [ebp-4]
leave
ret
您可以看到b的值已经通过edx传输。而默认eax包含a的值。
结构类型可以是函数返回值的类型。这是安全的,因为编译器将创建结构的副本,并返回该副本,而不是函数中的本地结构。
typedef struct{
int a,b;
}mystruct;
mystruct func(int c, int d){
mystruct retval;
cout << "func:" <<&retval<< endl;
retval.a = c;
retval.b = d;
return retval;
}
int main()
{
cout << "main:" <<&(func(1,2))<< endl;
system("pause");
}
安全性取决于结构本身的实现方式。我只是在实现类似的东西时偶然发现了这个问题,这就是潜在的问题。
编译器在返回值时会执行一些操作(可能还有其他操作):
- 调用复制构造函数
mystruct(const mystruct&)
(this
是编译器自身分配的函数func
外部的临时变量) - 对
func
内部分配的变量调用析构函数~mystruct
- 如果返回的值被分配给具有
=
的其他值,则调用mystruct::operator=
- 对编译器使用的临时变量调用析构函数
~mystruct
现在,如果mystruct
像这里描述的那样简单,那么一切都很好,但如果它有指针(如char*
)或更复杂的内存管理,那么这一切都取决于mystruct::operator=
、mystruct(const mystruct&)
和~mystruct
是如何实现的。因此,我建议在将复杂的数据结构作为值返回时要注意。
像您所做的那样返回结构是完全安全的。
然而,基于这句话:因为我的理解是,一旦函数的作用域结束,用于分配结构的任何堆栈都可以被覆盖,我只能想象一种情况,即结构的任何成员都是动态分配的(malloc'ed或new'ed),在这种情况下,如果没有RVO,动态分配的成员将被销毁,返回的副本将有一个指向垃圾的成员。
注意:这个答案只适用于c++11以后的版本。不存在";"C/C++";,它们是不同的语言
不,按值返回本地对象没有危险,建议这样做。但是,我认为这里的所有答案都缺少一个重要的点。许多其他人说,该结构要么被复制,要么使用RVO直接放置。然而,这并不完全正确。我将尝试解释在返回本地对象时可能发生的确切情况。
移动语义
从c++11开始,我们就有了rvalue引用,它是对可以安全窃取的临时对象的引用。例如,std::vector有一个移动构造函数和一个移动赋值运算符。这两者都具有恒定的复杂性,并且只需将指针复制到要移动的向量的数据。我不会在这里详细介绍移动语义。
因为在函数中本地创建的对象是临时的,并且在函数返回时会超出范围,所以返回的对象在c++11之后不会被复制。正在对返回的对象调用move构造函数(或不调用,稍后解释)。这意味着,如果您要返回一个具有昂贵的复制构造函数但价格低廉的移动构造函数的对象,如大向量,则只有数据的所有权从本地对象转移到返回的对象,这很便宜。
请注意,在您的特定示例中,复制和移动对象没有区别。结构的默认移动构造函数和复制构造函数会导致相同的操作;复制两个整数。然而,这至少比任何其他解决方案都快,因为整个结构适合64位CPU寄存器(如果我错了,请纠正我,我不知道多少CPU寄存器)。
RVO和NRVO
RVO的意思是返回值优化,是编译器为数不多的可能产生副作用的优化之一。由于c++17,因此需要RVO。当返回一个未命名的对象时,它是直接在调用者指定返回值的地方构造的。复制构造函数和移动构造函数都没有被调用。如果没有RVO,将首先在本地构造未命名对象,然后在返回的地址中移动构造,然后对本地未命名对象进行销毁。
需要RVO(c++17)或可能(在c++17之前)的示例:
auto function(int a, int b) -> MyStruct {
// ...
return MyStruct{a, b};
}
NRVO的意思是命名返回值优化,与RVO相同,只是它是为被调用函数的本地命名对象执行的。标准(c++20)仍然不能保证这一点,但许多编译器仍然这样做。请注意,即使有命名的本地对象,它们在返回时也会被移动。
结论
唯一应该考虑不按值返回的情况是,当您有一个命名的非常大(如堆栈大小)的对象时。这是因为NRVO尚未得到保证(截至c++20),即使移动对象也会很慢。我的建议,以及Cpp核心指南中的建议是,始终倾向于按值返回对象(如果有多个返回值,请使用struct(或tuple)),其中唯一的例外是当对象移动成本很高时。在这种情况下,请使用非常数引用参数。
返回一个必须从c++中的函数手动释放的资源从来都不是一个好主意。永远不要那样做。至少使用std::unique_ptr,或者使用释放其资源(RAII)并返回其实例的析构函数创建自己的非本地或本地结构。如果资源没有自己的移动语义(以及删除复制构造函数/赋值),那么定义移动构造函数和移动赋值操作符也是一个好主意。
返回结构是不安全的。我喜欢自己做,但如果有人稍后将复制构造函数添加到返回的结构中,则会调用复制构造函数。这可能是意外的,可能会破坏代码。这个bug很难找到。
我有一个更详细的答案,但主持人不喜欢。所以,根据你们的经验,我的建议很短。
让我们为这个问题添加第二部分:这会因编译器而异吗?
的确如此,正如我痛苦地发现的那样:http://sourceforge.net/p/mingw-w64/mailman/message/33176880/
我在win32(MinGW)上使用gcc来调用返回structs的COM接口。事实证明,MS的做法与GNU不同,所以我的(gcc)程序崩溃了,堆栈被砸碎了。
这可能是MS可能有更高的基础,但我所关心的是MS和GNU之间的ABI兼容性,以便在Windows上构建。
如果是,那么最新版本的台式机编译器的行为是什么:gcc、g++和Visual Studio
你可以在Wine邮件列表中找到一些关于MS如何做到这一点的消息。
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 为什么将一个结构的引用设置为等于另一个结构只会更改一个数据成员?
- 如何将一个结构的字符数组复制到结构的另一个字符数组中?
- 如何将数据从一个结构链接到另一个结构
- 是否可以使用智能指针成员设置具有另一个结构的结构?
- std::transform 将一个结构数组复制到另一个结构数组
- 输入 ctrl + x 后如何再次 cin (cin<<x) 循环(cin 一个结构)
- 如何在级别上打印树,给定一个结构,该结构保存节点的值及其父节点的值,对于树中的每个节点
- 创建一个结构的关联数组,以创建一个缓冲区,供键快速访问
- 如何在另一个结构中初始化结构数组?
- 在另一个结构中声明内部结构会导致错误:结构使用无效
- 我想重置一个C++结构统计,我可以以某种方式使用 stat() 语法吗?
- C++可以输出一个结构的内存地址吗
- 通过 C++ 中的另一个结构成员访问结构
- C++ push_back() 一个结构体到一个向量中
- 难以从另一个结构中的函数返回结构
- 如何将向量添加到结构中以创建一个库存系统,在该系统中,我可以仅使用一个结构向系统添加多种不同的葡萄酒
- 如何制作一个结构程序,在其中可以存储无限量的数据,以便每次您想要时都可以将另一个产品添加到列表中?
- 将一个结构的值复制到另一个结构
- 是否可以在同一未创建的结构内创建一个结构