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


82

我的理解是不应该这样做,但是我相信我已经看过了类似示例的示例(注意,代码在语法上不一定正确,但思想确实存在)

typedef struct{
    int a,b;
}mystruct;

然后是一个功能

mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

我了解,如果我们想执行以下操作,则应始终返回指向malloc结构的指针,但我很肯定看到了执行此操作的示例。它是否正确?就我个人而言,我总是返回指向malloc的结构的指针,或者只是通过对该函数的引用进行传递并在那里修改值。(因为我的理解是,一旦函数的作用域结束,用于分配结构的任何堆栈都将被覆盖)。

让我们在问题中添加第二部分:这是否因编译器而异?如果可以,那么最新版本的台式机编译器(gcc,g ++和Visual Studio)的行为是什么?

有什么想法吗?


33
谁说:“我知道这是不应该做的” 我一直在做。还要注意,typedef在C ++中不是必需的,并且不存在“ C / C ++”之类的东西。
PlasmaHH 2012年

4
这个问题似乎并不针对c ++。
长颈鹿队长2012年

4
@PlasmaHH在周围复制大型结构可能效率很低。这就是为什么在按值返回结构之前应该谨慎并认真思考的原因,尤其是如果结构具有昂贵的副本构造函数且编译器不擅长返回值优化的情况。我最近对一个应用程序进行了优化,该应用程序将大量时间花在复制构造函数上,用于一些程序员决定按值返回的大型结构。效率低下使我们需要购买额外的数据中心硬件,成本约为80万美元。
Crashworks 2012年

8
@Crashworks:恭喜,我希望你的老板给你加薪。
PlasmaHH 2012年

6
@Crashworks:确保总是不加思索地按值返回很不好,但是在自然的情况下,通常没有安全的替代方法也不需要复制,因此按值返回是最好的解决方案不需要任何堆分配。经常有甚至不会复制,使用编译好的复制省略的时候有可能和C ++ 11应该跳,移动语义可以消除更加深复制的。如果你做任何事情,这两种机制将无法正常工作,否则而是返回值。
2012年

Answers:


75

这是绝对安全的,这样做也没有错。另外:编译器不会改变它。

通常,当(如您的示例)您的结构不太大时,我会认为这种方法甚至比返回malloc的结构更好(这malloc是一个昂贵的操作)。


3
如果其中一个字段是char *还是安全的吗?现在结构中将有指针
jzepeda 2012年

3
@ user963258实际上,这取决于您如何实现复制构造函数和析构函数。
Luchian Grigore 2012年

2
@PabloSantaCruz这是一个棘手的问题。如果是考题,那么如果需要考虑所有权的话,考官很可能会希望回答“否”。
长颈鹿队长2012年

2
@CaptainGiraffe:是的。由于OP并没有阐明这一点,并且他/她的示例基本上是C,所以我认为它比C ++问题更像是一个C问题。
巴勃罗·圣克鲁斯

2
@Kos有些编译器没有NRVO?从哪一年开始?还要注意:在C ++ 11中,即使它没有NRVO,它也会调用move语义。
大卫

70

非常安全。

您正在按价值回报。导致未定义行为的是您是否通过引用返回。

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

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

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

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

因为我的理解是,一旦函数的作用域结束,用于分配结构的任何堆栈都将被覆盖

错了 我的意思是,这是正确的,但是您返回了在函数内部创建的结构的副本。从理论上讲。实际上,RVO可以而且很可能会发生。阅读返回值优化。这意味着尽管retval函数结束时似乎超出范围,但实际上可能是在调用上下文中构建的,以防止多余的复制。这是编译器可以自由实现的优化。


3
+1提及RVO。实际上,这一重要的优化使该模式对于具有昂贵的复制构造函数的对象(例如STL容器)而言是可行的。
科斯2012年

1
值得一提的是,尽管编译器可以自由执行返回值优化,但不能保证会。这不是您可以指望的,只有希望。
Watcom 2013年

-1表示“尽可能避免动态分配的内存。” 这往往是对于新手,规则,并经常导致代码中LARGE返回的数据量(和他们困惑为什么事情运行缓慢),当一个简单的指针可以节省大量的时间。在正确的规则是返回结构或指针基于速度,使用和清晰度
劳埃德萨金特

9

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


8

不仅可以安全地struct在C中返回a (或者class在C ++中返回struct-s实际上是class具有默认public:成员的-es ),而且很多软件都可以这样做。

当然,在class用C ++返回a时,该语言指定将调用某些析构函数或移动构造函数,但是在许多情况下,编译器可以对其进行优化。

另外,Linux x86-64 ABI指定通过寄存器(&)完成struct带有两个标量(例如,指针或long)值的返回a ,因此非常快速且高效。因此,对于这种特定情况,返回这样一个具有两个标量的字段可能比做其他任何事情都要快(例如,将它们存储到作为参数传递的指针中)。%rax%rdxstruct

这样,返回这样的两标量字段struct比-malloc对其进行返回并返回指针要快得多。


5

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


1
听说过返回值优化?
Luchian Grigore'3

是的,但是我们说的是按值返回struct的一般情况,并且在某些情况下编译器无法执行RVO。
ebutusov 2012年

3
我想说的只是在完成一些分析后才担心多余的副本。
Luchian Grigore'3

4

结构类型可以是函数返回的值的类型。这是安全的,因为编译器将创建struct的副本并返回该副本而不是函数中的本地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");
}

4

安全性取决于该结构本身是如何实现的。在执行类似的操作时,我偶然发现了这个问题,这就是潜在的问题。

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

  1. 调用复制构造函数mystruct(const mystruct&)this是编译器自身分配的函数之外的临时变量func
  2. ~mystruct在内部分配的变量上调用析构函数func
  3. 调用,mystruct::operator=如果返回值分配给其他=
  4. ~mystruct在编译器使用的临时变量上调用析构函数

现在,如果mystruct是那样简单,这里描述的一切都很好,但是如果它有指针(像char*)或更复杂的内存管理,那么这一切都取决于如何mystruct::operator=mystruct(const mystruct&)以及~mystruct实现。因此,我建议在将复杂数据结构作为值返回时要特别小心。


仅在c ++ 11之前为true。
比约恩Sundin的

4

完成操作后返回一个结构是绝对安全的。

但是,基于以下声明:因为我的理解是,一旦函数的作用范围结束,则用于分配结构的任何堆栈都将被覆盖,我可以想象只有一个动态分配结构成员的场景(在这种情况下,如果没有RVO,动态分配的成员将被销毁,并且返回的副本将具有指向垃圾的成员。


堆栈仅临时用于复制操作。通常,堆栈将在调用之前被保留,并且被调用函数将要返回的数据放入堆栈中,然后调用者从堆栈中提取此数据并将其存储在分配给它的任何位置。因此,那里没有后顾之忧。
Thomas Tempelmann 2012年

3

我也会同意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的值。


2

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

我的回答更为详尽,但主持人不喜欢它。因此,按您的经验,我的建议很短。


“返回结构并不安全。[…]复制构造函数将被调用。” –安全低效之间有区别。返回一个结构绝对是安全的。即使这样,编译器最有可能消除对复制ctor的调用,因为该结构是在调用者的堆栈上创建的。
phg

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似乎如何执行此操作的消息。


如果您提供了指向您所引用的Wine邮件列表的指针,将会更有帮助。
乔纳森·勒夫勒

返回结构很好。COM指定二进制接口; 如果有人不能正确实现COM,那将是一个错误。
MM 2015年

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核心准则》中的建议始终是按值返回对象(如果有多个返回值,则使用struct(或tuple)),唯一的例外是对象移动成本很高。在这种情况下,请使用非常量引用参数。

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


有趣的事实。我认为golang也有类似之处。
Mox
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.