ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

在 C++ 中,做类似的事情我有 2 个选项

(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;,这看起来不太好。我不想返回复制的值,因为它效率低下(我认为),所以问题来了

是否有第三种解决方案(不需要复制值)?

如果我坚持第一个解决方案有什么问题吗?

何时以及为什么应该使用第二种解决方案?

+1很好地提出了这个问题。
为了非常迂腐,说“函数返回一些东西”有点不准确。更准确地说,评估函数调用会产生一个值。该值始终是一个对象(除非它是一个 void 函数)。区别在于该值是泛左值还是纯右值——这取决于声明的返回类型是否为引用。

G
GManNickG

我不想返回复制的值,因为它效率低下

证明给我看。

查找 RVO 和 NRVO,以及 C++0x 移动语义。在 C++03 中的大多数情况下,out 参数只是让代码变得丑陋的好方法,而在 C++0x 中,使用 out 参数实际上会伤害自己。

只需编写干净的代码,按值返回。如果性能是一个问题,请对其进行分析(停止猜测),然后找出可以解决的方法。它可能不会从函数返回东西。

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

如果要使用动态分配,至少可以将其放入智能指针中。 (无论如何都应该这样做)然后你不用担心删除任何东西,事情是异常安全的,等等。唯一的问题是它可能比按值返回慢!


@phunehehe:毫无意义,您应该分析您的代码并找出答案。 (提示:不。)编译器非常聪明,如果不需要的话,他们不会浪费时间复制东西。即使复制要付出一些代价,你仍然应该争取好的代码而不是快速的代码;当速度成为问题时,好的代码很容易优化。为你不知道的东西丑化代码是没有意义的。特别是如果你真的放慢速度或什么也没得到。如果您使用的是 C++0x,移动语义使这不是问题。
@GMan,回复:RVO:实际上,只有当您的调用者和被调用者碰巧在同一个编译单元中时,这才是正确的,而在现实世界中,大多数情况下并非如此。因此,如果您的代码没有全部模板化(在这种情况下,它将全部在一个编译单元中)或者您有一些链接时优化(GCC 仅从 4.5 开始),您会感到失望。
@Alex:编译器在跨翻译单元的优化方面越来越好。 (VC 现在在几个版本中都这样做了。)
@Alex B:这完全是垃圾。许多非常常见的调用约定使调用者负责为大返回值分配空间,而被调用者负责它们的构造。即使没有链接时间优化,RVO 也能愉快地跨编译单元工作。
@Charles,经过检查,它似乎是正确的!我撤回了我明显错误的陈述。
A
Amir Rachum

只需创建对象并返回它

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

我认为如果您忘记优化而只编写可读代码(稍后您需要运行分析器 - 但不要预先优化),您会帮自己一个忙。


这在 C++98 中是如何工作的?我在 CINT 解释器上遇到错误,想知道这是由于 C++98 或 CINT 本身造成的......!
仅供参考:根据编译器的不同,这已使用 return value optimization 进行了优化
D
Destructor

只需返回一个像这样的对象:

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

这将调用 Things 上的复制构造函数,因此您可能希望自己实现它。像这样:

Thing(const Thing& aThing) {}

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

更新

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


Thing thing(); 声明了一个返回 Thing 的本地函数,此外,标准允许编译器在您提供的情况下省略复制构造函数;任何现代编译器都可能会这样做。
通过实现复制构造函数,您带来了一个好处,尤其是在需要深度复制的情况下。
+1 用于明确说明复制构造函数,尽管正如@dreamlax 所说,编译器很可能会“优化”函数的返回代码,避免不必要地调用复制构造函数。
在 2018 年,在 VS 2017 中,它正在尝试使用 move 构造函数。如果移动构造函数被删除而复制构造函数没有被删除,它将无法编译。
C
Community

您是否尝试过使用智能指针(如果 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 
    }


auto_ptr 已弃用;请改用 shared_ptrunique_ptr
只是要在此处添加此内容...我已经使用 c++ 多年了,尽管不是专业地使用 c++...我已决定不再尝试使用智能指针,它们只是 imo 的绝对混乱并导致所有各种各样的问题,也没有真正帮助加速代码。我宁愿自己使用 RAII 复制数据并自己管理指针。所以我建议如果可以的话,避免使用智能指针。
d
dreamlax

确定是否正在调用复制构造函数的一种快速方法是将日志记录添加到类的复制构造函数中:

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

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

呼叫someFunction;您将获得的“调用了复制构造函数”行的数量将在 0、1 和 2 之间变化。如果您没有得到,那么您的编译器已经优化了返回值(允许这样做)。如果你得到的结果不是 0,并且你的复制构造函数非常昂贵,然后搜索从你的函数返回实例的替代方法。


M
Matt Joiner

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

使用 shared_ptr。 Deref 它是一个指针。当对所包含事物的最后引用超出范围时,它将为您删除。

第一个解决方案在幼稚库中很常见。它有一些性能和语法开销,尽可能避免

仅当您可以保证不会抛出异常,或者当性能绝对关键时(您将在这甚至变得相关之前与 C 或程序集进行交互)时才使用第二种解决方案。


M
Muktadir Rahman

我不想返回复制的值,因为它效率低下

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

例如,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;
}

E
EMP

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