【问题标题】:How to declare a variable that stores an object returned by reference?如何声明一个存储引用返回的对象的变量?
【发布时间】:2012-12-21 07:45:42
【问题描述】:

C++ 引用仍然让我感到困惑。假设我有一个函数/方法,它创建一个 Foo 类型的对象并通过引用返回它。 (我假设如果我想返回对象,它不能是分配在堆栈上的局部变量,所以我必须用new在堆上分配它):

Foo& makeFoo() {
    ...
    Foo* f = new Foo;
    ...
    return *f;
}

当我想将创建的对象存储在另一个函数的局部变量中时,类型应该是Foo

void useFoo() {

    Foo f = makeFoo();
    f.doSomething();
}

Foo&?

void useFoo() {

    Foo& f = makeFoo();
    f.doSomething();
}

由于两者都是正确的语法:这两个变体之间是否存在显着差异?

【问题讨论】:

  • (I assume that if I want to return the object, it cannot be a local variable allocated on the stack, so I must allocate it on the heap with new) 这是一个问题,并且使情况变得更糟。简单地不要通过引用返回变量:通过值返回它并让优化器(或 C++11 移动语义)完成他们的工作。
  • 其实Foo& f = makeFoo();的版本可能编译不了。
  • @LightnessRacesinOrbit 为什么不呢?
  • @LuchianGrigore:参考临时?
  • @cls:任何正确解释移动语义学的资源都应该让您了解它的相关性(试试维基百科:“移动语义学”)。重点是减少不必要的复制成本。只需确保您的 Foo 对象旨在利用它。主要是要保证对象的“大”不在对象本身,而是在外部引用。一个典型的例子是std::vector,它在内部只是几个指针。但是这些指针可以引用大量数据。可以通过简单地复制和取消指针来移动向量。

标签: c++ reference return return-type


【解决方案1】:

是的,第一个将复制返回的引用,而第二个将是对makeFoo 返回的引用。

请注意,使用第一个版本会导致内存泄漏(很可能),除非您在复制构造函数中使用了一些黑魔法。

嗯,第二个也会导致泄漏,除非你打电话给delete &f;

底线:不要。随波逐流,按价值回报。或智能指针。

【讨论】:

  • 所以我应该保持原样,只需将makeFoo 的返回类型更改为Foo 而不是Foo&?
  • @cls:你在堆栈上分配 Foo 并返回一个副本!所以不涉及new
  • @Skalli 如果 Foo 是一个大对象而我没有内存或时间来复制它怎么办?
  • @cls 编译器可能会将其优化掉。如果您真的担心,请返回std::shared_ptr<Foo>
  • @cls:Luchian Grigore 更快,他是对的:使用智能指针。您可以使用几个不同的智能指针,每个都专注于一个特殊的用例。 std::shared_ptr 应该最适合您的用例。
【解决方案2】:

你的第一个代码做了很多工作:

void useFoo() {
    Foo f = makeFoo();  // line 2
    f.doSomething();
}

想到第 2 行,发生了一些有趣的事情。首先,编译器将发出代码以使用类的默认构造函数在f 处构造一个Foo 对象。然后,它将调用makeFoo(),它还会创建一个新的Foo 对象并返回对该对象的引用。编译器还必须发出代码,将makeFoo() 的临时返回值复制到f 的对象中,然后它会销毁临时对象。一旦第 2 行完成,就会调用 f.doSomething()。但就在useFoo() 返回之前,我们也销毁了f 处的对象,因为它超出了范围。

您的第二个代码示例效率更高,但实际上可能是错误的:

void useFoo() {
    Foo& f = makeFoo();   // line 2
    f.doSomething();
}

考虑该示例中的第 2 行,我们意识到我们没有为 f 创建对象,因为它只是一个引用。 makeFoo() 函数返回一个新分配的对象,我们保留对它的引用。我们通过该引用调用doSomething()。但是当useFoo() 函数返回时,我们永远不会破坏makeFoo() 为我们创建的新对象并且它会泄漏。

有几种不同的方法可以解决此问题。如果您不介意额外的构造函数、创建、复制和销毁,您可以只使用第一个代码片段中的引用机制。 (如果您有微不足道的构造函数和析构函数,并且没有太多(或没有)要复制的状态,那么这并不重要。)您可以只返回一个指针,这强烈暗示调用者负责管理生命周期被引用对象的循环。

如果你返回一个指针,你暗示调用者必须管理对象的生命周期,但你没有强制执行它。总有一天,某个地方的某个人会出错。因此,您可能会考虑创建一个包装类来管理引用并提供访问器来封装对象的管理。 (如果您愿意,您甚至可以将其烘焙到Foo 类本身中。)这种类型的包装类在其通用形式中称为“智能指针”。如果您使用的是 STL,您将在 the std::unique_ptr template class 中找到智能指针实现。

【讨论】:

  • 我基本同意,但您可以在最后一段中给出如此巧妙的提示。这将降低复制成本,并在智能指针超出范围时释放对象。
  • 没错,斯卡利。为此,我将其锐化了一点。
  • 其实第一个useFoo的描述是错误的。编译器不会向默认构造 f 发出代码。它只会留出足够的内存来复制构造f,结果来自makeFoo
  • @Bart van Ingen Schenau,这是由标准保证的,还是由实现定义的?还是取决于class foo 实现了哪些构造函数和运算符?
  • @MikeB:标准保证useFoo中不会使用默认构造函数。留出内存时的细节取决于实现。
【解决方案3】:

函数永远不应该返回对创建的新对象的引用。当你创建一个新值时,你应该返回一个值或一个指针。返回一个值几乎总是首选,因为几乎所有编译器都会使用 RVO/NRVO 来消除额外的副本。

返回一个值:

Foo makeFoo(){
    Foo f;
    // do something
    return f;
}

// Using it
Foo f = makeFoo();

返回一个指针:

Foo* makeFoo(){
    std::unique_ptr<Foo> p(new Foo());  // use a smart pointer for exception-safety
    // do something
    return p.release();
}

// Using it
Foo* foo1 = makeFoo();                 // Can do this
std::unique_ptr<Foo> foo2(makeFoo());   // This is better

【讨论】:

  • 对于第二个示例,为什么不直接返回 unique_ptr 并将放弃安全的决定留给调用者?
  • 我更愿意将如何使用它的决定留给调用者。返回一个原始指针可以让他们将它放入他们想要的任何类型的智能指针中。如果函数和调用者没有针对完全相同的标准库版本进行编译,您也不太可能遇到 ABI 问题。
  • 返回unique_ptr 还允许他们将其放入他们想要的任何类型的智能指针中(他们可以像在函数中那样调用release),并且它是自我记录的。当一个函数返回一个原始指针时,我不能对需要采取什么行动来确保它得到正确管理做出任何假设。它可能是用 new 或 malloc 动态分配的,也可能根本不需要管理。至于你的第二点,我只能说我个人从来没有遇到过这样的问题,但我不会怀疑。
猜你喜欢
  • 2015-03-17
  • 1970-01-01
  • 2023-04-01
  • 1970-01-01
  • 2012-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-03
相关资源
最近更新 更多