如何在 C + + 中“返回一个对象”?

我知道这个标题听起来很熟悉,因为有很多类似的问题,但是我要问的是这个问题的不同方面(我知道在堆栈中放置东西和将它们放在堆中之间的区别)。

在 Java 中,我总是可以返回对“本地”对象的引用

public Thing calculateThing() {
Thing thing = new Thing();
// do calculations and modify thing
return thing;
}

在 C + + 中,要做类似的事情,我有两个选择

(1)当我需要“返回”一个对象时,我可以使用引用

void calculateThing(Thing& thing) {
// do calculations and modify thing
}

那就这样用

Thing thing;
calculateThing(thing);

(2)或者我可以返回一个指向动态分配对象的指针

Thing* calculateThing() {
Thing* thing(new Thing());
// do calculations and modify thing
return thing;
}

那就这样用

Thing* thing = calculateThing();
delete thing;

使用第一种方法,我不必手动释放内存,但对我来说,它使代码难以阅读。第二种方法的问题是,我必须记住 delete thing;,它看起来不太好。我不想返回一个复制的值,因为它是低效的(我认为) ,所以问题来了

  • 是否有第三种解决方案(不需要复制值) ?
  • 如果我坚持第一个解决方案,有什么问题吗?
  • 何时以及为什么我应该使用第二种解决方案?
99518 次浏览

我不想返回一个复制的值,因为它是低效的

证明给我看。

查找 RVO 和 NRVO,在 C + + 0x 中查找 move-semantic。在大多数情况下,在 C + + 03中,out 参数只是使代码难看的一种好方法,而在 C + + 0x 中,使用 out 参数实际上会伤害到自己。

只需编写干净的代码,按值返回。如果性能是一个问题,分析它(停止猜测) ,并找到您可以做些什么来解决它。它可能不会从函数返回东西。


也就是说,如果您一定要这样写,那么可能需要使用 out 参数。它避免了动态内存分配,这种分配更安全,而且通常更快。它确实需要您在调用函数之前有一些方法来构造对象,这并不总是对所有对象都有意义。

如果要使用动态分配,至少可以将其放在智能指针中。这样你就不用担心删除任何东西,事情是异常安全的,等等。唯一的问题是它可能比返回值慢!

我相信 C + + 专家会给出一个更好的答案,但我个人喜欢第二种方法。使用智能指针有助于解决遗忘 delete的问题,正如您所说的,它看起来比必须事先创建一个对象(如果您想在堆上分配它,仍然必须删除它)更干净。

首先,你有一个错误的代码,你的意思是有 Thing *thing(new Thing());,只有 return thing;

  • 使用 shared_ptr<Thing>。把它当作一个指针。当包含的最后一个对 Thing的引用超出作用域时,它将被删除。
  • 第一种解决方案在幼稚库中非常常见,它具有一定的性能和语法开销,尽可能避免使用它
  • 只有当您能够保证不会抛出异常,或者性能绝对关键时(在此变得相关之前,您将与 C 或汇编进行接口) ,才能使用第二种解决方案。

只要创建对象并返回它

Thing calculateThing() {
Thing thing;
// do calculations and modify thing
return thing;
}

我认为,如果您忘记优化,只是编写可读的代码(稍后您将需要运行分析器,但不要预优化) ,这将对您自己有利。

你是否尝试过使用智能指针(如果 Thing 真的很大很重的话) ,比如 share _ ptr:




std::shared_ptr calculateThing()
{
std::shared_ptr<Thing> thing(new Thing);
// .. some calculations
return thing;
}
    

// ...
{
std::shared_ptr<Thing> thing = calculateThing();
// working with thing
    

// shared_ptr frees thing
}


一个快速判断复制建构子是否被调用的方法是在类的复制建构子中添加日志记录:

MyClass::MyClass(const MyClass &other)
{
std::cout << "Copy constructor was called" << std::endl;
}


MyClass someFunction()
{
MyClass dummy;
return dummy;
}

调用 someFunction,你将得到的“复制建构子被调用”的行数将在0、1和2之间变化。如果没有返回值,那么编译器已经优化了返回值(这是允许的)。如果你得不到0,并且你的复制建构子非常昂贵,那么 那么会寻找其他方法来从你的函数中返回实例。

只要返回这样一个对象:

Thing calculateThing()
{
Thing thing();
// do calculations and modify thing
return thing;
}

这将调用 Things 的复制建构子,因此您可能需要自己实现它。像这样:

Thing(const Thing& aThing) {}

这可能会执行得慢一些,但可能根本不是问题。

更新

编译器可能会优化对复制建构子的调用,因此不会有额外的开销。(就像 Dreamlax 在评论中指出的那样)。

我不想返回一个复制的值,因为它是低效的

这可能不正确。编译器可以进行优化以防止这种复制。

例如,GCC 进行这种优化。在下面的程序中,既不调用 move 构造函数也不调用复制建构子,因为不执行复制或移动操作。另外,请注意 c的地址。即使对象 c在函数 f()内被实例化,c仍驻留在 main()的堆栈帧中。

class C {
public:
int c = 5;
C() {}
C(const C& c) {
cout << "Copy constructor " << endl;
}
C(const C&& c)  noexcept {
cout << "Move Constructor" << endl;
}
};


C f() {
int beforeC;
C c;
int afterC;


cout << &beforeC << endl;   //0x7ffee02f26ac
cout << &c << endl;         //0x7ffee02f2710 (notice: even though c is instantiated inside f(), c resides in the stack frame of main()
cout << &afterC << endl;    //0x7ffee02f26a8


return c;
}


C g() {
C c = f(); ///neither copy constructor nor move constructor of C are called, since none is done
cout << &c << endl;  //0x7ffee02f2710
return c;
}


int main() {
int beforeC;
C c = g();    ///neither copy constructor nor move constructor of C are called, since none is done
int afterC;


cout << &beforeC << endl; //0x7ffee02f2718
cout << &c << endl;       //0x7ffee02f2710 (notice:even though c is returned from f,it resides in the stack frame of main)
cout << &afterC << endl;  //0x7ffee02f270c
return 0;
}