用C或C++返回一个结构安全吗

Is it safe to return a struct in C or C++?

本文关键字:一个 结构 安全 C++ 返回      更新时间:2023-10-16

我所理解的是不应该这样做,但我相信我见过这样的例子(注意代码不一定语法正确,但想法是存在的)

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可能比执行任何其他操作(例如,将它们存储到作为参数传递的指针中)更快。

返回这样一个双标量字段structmalloc返回它并返回指针快得多。

这是完全合法的,但对于大型结构,需要考虑两个因素:速度和堆栈大小。

我也同意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");
}

安全性取决于结构本身的实现方式。我只是在实现类似的东西时偶然发现了这个问题,这就是潜在的问题。

编译器在返回值时会执行一些操作(可能还有其他操作):

  1. 调用复制构造函数mystruct(const mystruct&)this是编译器自身分配的函数func外部的临时变量
  2. func内部分配的变量调用析构函数~mystruct
  3. 如果返回的值被分配给具有=的其他值,则调用mystruct::operator=
  4. 对编译器使用的临时变量调用析构函数~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如何做到这一点的消息。

相关文章: