在C或C ++中返回结构是否安全?

时间:2012-03-06 19:52:40

标签: c++ c function struct return-type

我理解的是,这不应该这样做,但我相信我已经看过这样做的例子(注意代码在语法上不一定正确,但想法就在那里)

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?

关于此事的想法?

12 个答案:

答案 0 :(得分:71)

这是非常安全的,这样做并没有错。另外:它不会因编译器而异。

通常情况下,当(像你的例子)你的结构不是太大时,我会认为这种方法甚至比返回malloc结构更好(malloc是一项昂贵的操作)。

答案 1 :(得分:66)

这是非常安全的。

你是按价值回归的。导致未定义行为的原因是您通过引用返回。

//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 struct的指针

你不应该。你应该尽可能避免动态分配内存。

  

或只是通过引用函数进行传递并修改值   那里。

此选项完全有效。这是一个选择问题。通常,如果要在修改原始结构时从函数中返回其他内容,则执行此操作。

  

因为我的理解是,一旦函数的范围是   结束,无论用于分配结构的堆栈都可以   覆盖

这是错误的。我的意思是,它是正确的,但你返回一个你在函数内创建的结构的副本。的理论上即可。在实践中, RVO 可能并且可能会发生。阅读返回值优化。这意味着虽然retval在函数结束时似乎超出了范围,但它实际上可能是在调用上下文中构建的,以防止额外的副本。这是编译器可以自由实现的优化。

答案 2 :(得分:9)

离开函数时,函数中mystruct对象的生命周期确实结束了。但是,您在return语句中按值传递对象。这意味着将对象从函数中复制到调用函数中。原始对象消失了,但副本仍然存在。

答案 3 :(得分:7)

不仅可以安全地在C中返回struct(或在C ++中返回class,其中struct - s实际为class - es,默认为{{ 1}}成员),但很多软件正在这样做。

当然,在C ++中返回public:时,语言指定会调用一些析构函数或移动构造函数,但在很多情况下,编译器可以对其进行优化。

此外,Linux x86-64 ABI指定通过寄存器({{}}返回带有两个标量的class(例如指针或struct)值。 {1}}& long)因此非常快速有效。因此,对于特定情况,返回这样的双标量字段%rax可能比执行任何其他操作更快(例如将它们存储到作为参数传递的指针中)。

返回这样一个双标量字段%rdxstruct更快,并返回一个指针。

答案 4 :(得分:5)

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

答案 5 :(得分:4)

结构类型可以是函数返回的值的类型。它是安全的,因为编译器将创建struct的副本并返回副本而不是函数中的本地结构。

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");
}

答案 6 :(得分:4)

像你一样返回一个结构是完全安全的。

但是基于这个语句:因为我的理解是,一旦函数的范围结束,用于分配结构的任何堆栈都可以被覆盖,我想象的只有一个场景,其中任何一个结构成员的动态分配(malloc'ed或new'ed),在这种情况下,没有RVO,动态分配的成员将被销毁,返回的副本将有一个指向垃圾的成员。

答案 7 :(得分:4)

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

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

  1. 调用复制构造函数mystruct(const mystruct&)this外部的临时变量编译器本身分配的函数func
  2. ~mystruct
  3. 中分配的变量调用析构函数func 如果使用mystruct::operator= 将返回的值分配给其他内容,则
  4. 调用=
  5. 对编译器使用的临时变量调用析构函数~mystruct
  6. 现在,如果mystruct与此处描述的一样简单,那么一切都很好,但如果它有指针(如char*)或更复杂的内存管理,那么这一切都取决于{{1实现了{},mystruct::operator=mystruct(const mystruct&)。 因此,我建议在将复杂数据结构作为值返回时要小心。

答案 8 :(得分:3)

我也同意sftrabbit,Life确实结束并且堆栈区域被清除但编译器足够聪明以确保所有数据都应该以寄存器或其他方式检索。

下面给出了一个简单的确认示例。(取自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。

的值

答案 9 :(得分:2)

返回结构是不安全的。我喜欢自己做,但如果有人稍后将复制构造函数添加到返回的结构中,则将调用复制构造函数。这可能是意料之外的,可能会破坏代码。这个bug很难找到。

我有更精心的答案,但主持人不喜欢它。所以,在您的开支中,我的提示很短。

答案 10 :(得分:2)

  

让我们在问题中添加第二部分:这是否因编译器而异?

确实如此,正如我发现的痛苦:   http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

我在win32(MinGW)上使用gcc来调用返回结构的COM接口。事实证明,MS对GNU的做法不同,因此我的(gcc)程序崩溃了。

可能MS可能在这里有更高的优势 - 但我关心的是MS和GNU之间的ABI兼容性,以便在Windows上构建。

  

如果是,那么桌面编译器的最新版本的行为是什么:   gcc,g ++和Visual Studio

您可以在Wine邮件列表中找到有关MS似乎如何执行此操作的消息。

答案 11 :(得分:1)

注意:此答案仅适用于c ++ 11及更高版本。没有“ C / C ++”之类的东西,它们是不同的语言。

否,按值返回本地对象没有危险,建议这样做。但是,我认为这里所有答案中都缺少一个重要的观点。许多其他人说该结构是使用RVO复制或直接放置的。但是,这并不完全正确。我将尽力解释返回本地对象时可能发生的事情。

移动语义

自c ++ 11起,我们有了右值引用,这些引用是可以从安全地窃取的临时对象的引用。例如,std :: vector具有移动构造函数和移动赋值运算符。两者都具有恒定的复杂度,并且只需将指针复制到要从其移出的向量的数据即可。在这里,我不会详细介绍移动语义。

由于在函数内本地创建的对象是临时对象,并且在函数返回时超出范围,因此从c ++ 11开始不再复制返回的对象。在返回的对象上调用move构造函数(或以后不再说明)。这意味着,如果要使用昂贵的复制构造函数但使用廉价的move构造函数(如大向量)返回对象,则只有数据所有权从本地对象转移到返回的对象上-这很便宜。

请注意,在您的特定示例中,复制和移动对象之间没有区别。结构的默认移动和复制构造函数将执行相同的操作。复制两个整数。但是,这至少比其他任何解决方案都快,因为整个结构都适合64位CPU寄存器(如果我输入错了,请更正我,我对CPU寄存器的了解不多。)

RVO和NRVO

RVO表示“返回值优化”,是编译器执行的可能会产生副作用的极少数优化之一。从c ++ 17开始,需要RVO。返回未命名的对象时,将直接在原位构造该函数,调用者将其分配返回的值。复制构造函数和move构造函数均未调用。如果没有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 Core Guidelines中的建议始终是按值返回对象(如果有多个返回值,则使用struct(或tuple)),唯一的例外是对象移动成本高昂。在这种情况下,请使用非常量引用参数。

返回必须从c ++中的函数手动释放的资源永远不是一个好主意。绝对不要那样做。至少使用std :: unique_ptr,或使用释放其资源(RAII)的析构函数制作自己的非本地或本地结构,并返回该实例的实例。如果资源没有自己的移动语义(并删除副本构造函数/赋值),那么定义移动构造函数和移动赋值运算符也是一个好主意。