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

194

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

在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;

使用第一种方法,我不需要手动释放内存,但对我来说它使代码难以阅读。第二种方法的问题是,我需要记住删除thing;,这看起来并不好。我不想返回复制的值,因为这样效率很低(我认为),所以就有了以下问题:

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

42
感谢您清晰简洁地提出问题。+1 - Kangkan
1
严谨地说,“函数返回某些东西”这种说法有点不准确。更正确的说法是,评估函数调用会产生一个值。该值始终是一个对象(除非它是一个void函数)。区别在于该值是glvalue还是prvalue——这取决于声明的返回类型是否为引用。 - Kerrek SB
8个回答

116
我不想返回复制的值,因为这样效率低下。
证明一下。
查找RVO和NRVO以及C++0x中的移动语义。在C++03中,在大多数情况下,一个输出参数只会让你的代码变得丑陋,在C++0x中,使用输出参数实际上会对自己造成伤害。
只需编写简洁的代码,通过值返回。如果性能有问题,请对其进行分析(停止猜测),并找出可以解决性能问题的方法。这很可能不是从函数中返回东西。
话虽如此,如果你坚定地要那样写,你可能需要使用输出参数。这避免了动态内存分配,这样更安全,通常也更快。但这确实需要在调用函数之前有某种方式来构造对象,而这并不总是对所有对象都有意义。
如果你想使用动态分配,至少可以将其放入智能指针中。(无论什么情况下都应该这样做)然后你就不用担心删除任何东西,事情是异常安全的等等。唯一的问题是,这很可能比按值返回还要慢!

13
@phunehehe: 不要瞎猜,你应该对你的代码进行剖析并找出问题所在。(提示:没有问题)编译器非常聪明,如果不必要它们不会浪费时间来复制东西。即使复制操作有一定代价,你仍应该追求良好的代码而非快速的代码;当速度成为问题时,良好的代码容易进行优化。如果你对某个可能不存在的问题强行修改代码,特别是如果这样做会拖慢程序或者毫无帮助时,这就是徒劳无益的。如果你使用C++0x,移动语义会解决这个问题。 - GManNickG
2
@Alex:编译器在跨翻译单元的优化方面越来越好。(VC现在已经做到了几个版本。) - sbi
9
@Alex B: 这完全是胡说八道。许多常见的调用约定要求调用者负责为大返回值分配空间,而被调用方负责构造这些返回值。即使没有链接时间优化,RVO也可以在编译单元之间正常工作。 - CB Bailey
1
通过使用类,您可以消除所有不必要的复制。在返回时,对象是从函数内部“移动”到外部,而不是复制。我在我的复制和交换答案中给出了一个真实的移动语义示例(甚至进行了逐步优化示例)。对于这个概念的高层次思考方式:复制表示“我正在复制你的资源,你保留你的资源”,移动表示“我正在取走你的资源,你没有任何资源”。移动非常简单(请查看我链接的答案中的代码)。 - GManNickG
6
@Charles,经过检查,似乎是正确的!我收回了我明显不准确的说法。 - Alex B
显示剩余6条评论

50

只需创建对象并返回它即可

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

如果你忘记优化,只是编写可读性高的代码(稍后可以运行分析器-但不要预先优化),我认为这将对您有帮助。


这在C++98中如何工作?我在CINT解释器上遇到了错误,想知道是由于C++98还是CINT本身...! - xcorat
请注意:根据编译器的不同,此代码可能会使用返回值优化进行优化。 - ViniciusArruda

22

只需返回一个如下的对象:

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

这将会调用Things类的复制构造函数,所以你可能想要自己实现它。像这样:

Thing(const Thing& aThing) {}

这可能会导致执行速度稍慢,但这可能根本不是问题。

更新

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


9
Thing thing(); 声明一个返回 Thing 类型的本地函数,此外,标准允许编译器在你所展示的情况下省略复制构造函数;任何现代编译器都可能会这样做。 - dreamlax
2
你提出了一个很好的观点,特别是在需要深拷贝时实现复制构造函数。 - mbadawi23
+1 是因为明确提到了复制构造函数,尽管正如 @dreamlax 所说,编译器很可能会“优化”返回函数的代码,避免不必要地调用复制构造函数。 - jose.angel.jimenez
在2018年的VS 2017中,尝试使用移动构造函数。如果移动构造函数被删除而拷贝构造函数没有,编译就会失败。 - Andrew

13

你尝试使用智能指针(如果Thing是一个非常庞大和重的对象),例如shared_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 
    }


4
auto_ptr 已被弃用,请改用 shared_ptrunique_ptr - MBraedley
我想插个话...虽然我已经使用C++有好几年了,但我并没有专门使用过C++。我已决定尝试不再使用智能指针,因为在我看来它们实在是一团糟,会导致各种问题,而且并不能真正加快代码的速度。我更愿意手动复制数据和管理指针,使用RAII。所以,如果可能的话,请避免使用智能指针。 - Andrew

9

判断是否调用了复制构造函数的一种快速方法是在类的复制构造函数中添加日志记录:

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

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

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


1

首先,您的代码中存在错误,您的意思应该是Thing *thing(new Thing());,然后只有return thing;

  • 使用shared_ptr<Thing>。像指针一样去引用它,当包含Thing的最后一个引用超出范围时,它将为您删除。
  • 第一个解决方案在幼稚的库中非常普遍。它带有一些性能和语法开销,请尽可能避免使用。
  • 仅在可以保证不会抛出异常或者性能绝对关键(在甚至变得相关之前,您将与C或汇编交互)时才使用第二个解决方案。

1

我不想返回一个复制的值,因为这样效率低下。

这可能并不是真的。编译器可以进行优化来防止这种复制。

例如,GCC就做到了这一点。在下面的程序中,既没有调用移动构造函数也没有调用拷贝构造函数,因为没有进行任何复制或移动操作。同时,注意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;
}

0
我相信会有C++专家提供更好的答案,但就我个人而言,我喜欢第二种方法。使用智能指针有助于解决忘记delete的问题,而且正如你所说,它看起来比先创建对象(如果要在堆上分配,则仍需删除)更加简洁。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接