【问题标题】:Cannot return std::unique_ptr from function without move不能不移动就从函数返回 std::unique_ptr
【发布时间】:2019-04-05 06:39:57
【问题描述】:

我正在更新一些使用 auto_ptr 的旧代码以改用 unique_ptr。这主要是一项搜索和替换工作,但我发现在代码返回 unique_ptr 的地方出现编译错误。

这是一个说明问题的示例:

#include <string>
#include <iostream>
#include <memory>
#include <utility>

using namespace std;

struct Foo {
    unique_ptr<string> us;

    Foo() {
        this->us = unique_ptr<string>(new string("hello"));
    }

    unique_ptr<string> bar() {
        return this->us;
    }

};


int main(int argc, const char **argv) {

    Foo foo;
    unique_ptr<string> s = foo.bar();
    cout << *s << endl;

}

当我编译它时,我得到了这个:

t1.cpp: In member function ‘std::unique_ptr<std::basic_string<char> > Foo::bar()’:
t1.cpp:17:16: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::basic_string<char>; _Dp = std::default_delete<std::basic_string<char> >]’
   return this->us;
                ^~
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/memory:80:0,
                 from t1.cpp:4:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/unique_ptr.h:388:7: note: declared here
       unique_ptr(const unique_ptr&) = delete;
       ^~~~~~~~~~

如果我改变错误的行来指定这样的移动:

        return move(this->us);

然后就可以了。

我发现多个引用表明不需要移动 - 例如来自 chromium 项目的 this SO questionthese guidelines

我的问题是:为什么在这种情况下需要明确指定移动?是否与我以某种方式返回实例变量的值有关?

如果这是一个骗局,请提前道歉 - 我确信以前会被问到,但我很难找到找到它的搜索词。

【问题讨论】:

  • 如果编译器在不告诉你的情况下将其转移到你身上,它可能会意外地破坏你的类不变量。
  • 这不是正确的骗局。 OP 想同时在两个不同的地方使用指针。所以这不是关于返回 unique_ptr,而是使用 shared_ptr !
  • @Christophe:链接的帖子建议使用shared_ptr

标签: c++ unique-ptr


【解决方案1】:

一切都与语义有关。 unique_ptr 是一个唯一 所有权,所以它要么被退回,要么在us 中,但不能同时返回。幸运的是,标准的规则可以保护您免受这种误解。

您可以做的是返回一个临时的unique_ptr(将被移动):

unique_ptr<string> bar() {
    return make_unique<string>("hello, world");
}

但是,如果您在多个地方需要unique_ptr 的副本(根据您的main() 似乎就是这种情况),那么您不是在寻找唯一的而是共享所有权。所以你应该去shared_ptr

struct Foo {
    shared_ptr<string> us;

    Foo() {
        us = make_shared<string>("hello, world");
    }
    shared_ptr<string> bar() {
        return us;
    }
};

int main(int argc, const char **argv) {
    Foo foo;
    auto s = foo.bar();
    cout << *s << endl;
}

【讨论】:

  • std::shared_ptr 消除了考虑所有权和生命周期的需要,反过来,在 99% 的情况下,当不清楚谁拥有什么并且对象由于循环而无法销毁时,这反过来会导致糟糕的设计参考。我建议尽可能避免使用std::shared_ptr。此外,std::shared_ptr 是性能最差的指针之一,因为它的大小是普通指针的两倍,加上带有两个原子计数器的控制块,并且它在复制时总是使用原子增量,在销毁时使用繁忙循环来减量,即使指针永远不要跨线程。
  • @MaximEgorushkin 我完全同意你在选择使用 shared_ptr 之前需要仔细考虑所有权!我还假装如果所有者获得了一个 unique_ptr 它不应该披露它。所以公开暴露它,试图返回它,或者试图返回底层指针已经表明了设计中的一个弱点。然而,罗马不是一天建成的,OP 提到要维护一个遗留代码库。我认为迁移到 shared_ptr 和 unique_ptr 将有助于 OP 更好地反映所有权,并在后期逐步重构以改进设计。
【解决方案2】:

你找到的所有引用都提到返回的对象必须是函数局部变量。你返回一个成员。成员变量在返回时不会被隐式视为右值。

这不是没有道理的。一个类通常不希望失去其资源的所有权,除非明确

当函数返回时,本地函数无论如何都会死掉,因此让编译器隐式蚕食它是值得优化的。

【讨论】:

    【解决方案3】:

    您正在尝试返回 Foo::us 类型的成员变量 std::unique_ptr&lt;&gt;std::unique_ptr&lt;&gt; 的唯一目的是不可复制但可移动。这就是为什么返回需要std::moveFoo::us 成员变量重置为null

    bar 函数可能应该通过值或常量引用返回 std::string

    string const& bar() const {
        return *this->us;
    }
    

    然后:

    int main() {
        Foo foo;
        string const& s = foo.bar();
        cout << s << endl;
    }
    

    【讨论】:

      【解决方案4】:

      正确使用std::auto_ptrstd::unique_ptr 具有相同的用途,它拥有对单个对象的唯一引用,该对象在指针销毁时被删除。

      std::auto_ptr 早于移动语义,它有一个复制构造函数,其行为类似于移动构造函数。很容易不小心复制了 std::auto_ptr 而没有意识到原来的 std::auto_ptr 现在是空的。

      由于std::unqiue_ptr 只能移动且不可复制,因此可以避免这种情况。您必须使用 std::move 明确放弃对指针的所有权。

      例外情况是分配了std::unique_ptrs 的本地堆栈,无论如何这些都将在当前范围的末尾被销毁,因此该语言允许您在不使用std::move 的情况下返回std::unique_ptr。由于您的 us 变量不是局部变量,因此您不能直接返回它以防止您意外释放所有权。

      【讨论】:

      • 这是一个很好的解释,说明为什么 auto_ptr 在语义不变的情况下比它应该更宽松!对于所有维护旧代码库的人来说,这是绝对要知道的。
      【解决方案5】:

      这显示了 auto_ptr 的确切危险以及它被删除的原因。

      如果您想象两次调用该函数,您会发现问题所在。

      auto_ptr<string> Foo::bar() {
          return this->us;
      }
      
      int func() {
        Foo foo;
        auto_ptr<string> a = foo.bar();
        auto_ptr<string> b = foo.bar();//uh oh, null ptr
      }
      

      如果你喜欢这种行为,你可以很容易地用 unique_ptr 模拟它

      #include <string>
      #include <iostream>
      #include <memory>
      #include <utility>
      
      using namespace std;
      
      struct Foo {
          unique_ptr<string> us;
      
          Foo() {
              this->us = unique_ptr<string>(new string("hello"));
          }
      
          unique_ptr<string> bar() {
              unique_ptr<string> local_us(us.release());
              return local_us;
          }
      
      };
      
      
      int main(int argc, const char **argv) {
      
          Foo foo;
          unique_ptr<string> a = foo.bar();
          unique_ptr<string> b = foo.bar();//b is a nullptr
          cout << *s << endl;
      
      }
      

      【讨论】:

      • unique_ptr&lt;string&gt; local_us(us.release()); return local_us; - 这就是return move(us); 所做的,但有风格。
      • @MaximEgorushkin 我的印象是从同一个对象移动两次不是一个好主意,而且“有效但未指定的状态”似乎不是将对象留在其中的最佳状态。
      • @PeterT 通常,被移动的对象处于未指定的状态,但特定的类可以定义该状态是什么,在unique_ptr 的情况下,该状态是空指针
      • @PeterT 正如 Alan 所提到的,从 unique_ptr 移动的是 null。不是因为标准要求(我认为不需要),而是因为null 以外的任何值都是错误的。通常,从状态移动到默认构造对象状态,除非该类没有默认构造函数。将move 视为swap 很有用,但可能会立即销毁rhs
      • @MaximEgorushkin 标准确实将它定义为 null,但如果它没有,新构造的对象也是正确的
      猜你喜欢
      • 2022-10-21
      • 1970-01-01
      • 1970-01-01
      • 2014-04-06
      • 1970-01-01
      • 2011-05-18
      相关资源
      最近更新 更多