【问题标题】:Reasons to return reference to std::unique_ptr返回对 std::unique_ptr 的引用的原因
【发布时间】:2018-03-05 17:39:21
【问题描述】:

我想知道是否有任何正当理由在 C++ 中通过引用返回唯一指针,即std::unique_ptr<T>&

我以前从未真正见过这种技巧,但我得到的新项目似乎大量使用了这种模式。乍一看,它只是有效地打破/规避了“唯一所有权”合同,从而无法在编译时捕获错误。考虑以下示例:

class TmContainer {
public:
    TmContainer() {
        // Create some sort of complex object on heap and store unique_ptr to it
        m_time = std::unique_ptr<tm>(new tm());
        // Store something meaningful in its fields
        m_time->tm_year = 42;
    }

    std::unique_ptr<tm>& time() { return m_time; }

private:
    std::unique_ptr<tm> m_time;
};

auto one = new TmContainer();
auto& myTime = one->time();
std::cout << myTime->tm_year; // works, outputs 42
delete one;
std::cout << myTime->tm_year; // obviously fails at runtime, as `one` is deleted

请注意,如果我们只返回std::unique_ptr&lt;tm&gt;(不是引用),它会引发明显的编译时错误,或者会强制使用移动语义:

// Compile-time error
std::unique_ptr<tm> time() { return m_time; }

// Works as expected, but m_time is no longer owned by the container
std::unique_ptr<tm> time() { return std::move(m_time); }

我怀疑一般的经验法则是所有此类情况都需要使用std::shared_ptr。我说的对吗?

【问题讨论】:

  • 返回对拥有 std::unique_ptr 的引用没有任何意义,恕我直言。
  • 您将返回对unique_ptr 的引用,原因与您返回对任何其他类型对象的引用相同。因为你想让调用者能够操纵它。
  • @KillzoneKid:是的。使用shared_ptr(按值返回),您无法重置原始所有者的指针。
  • 我在代码审查中不接受此代码。只需返回存储在其中的原始指针。
  • 这闻起来重构变坏了。好像有人的任务是替换代码库中的所有原始指针,并支持unique_ptr

标签: c++ shared-ptr unique-ptr


【解决方案1】:

这有两个用例,我认为这表明设计不佳。拥有非常量引用意味着您可以窃取资源或替换它,而无需提供单独的方法。

// Just create a handle to the managed object
auto& tm_ptr = tm_container.time();
do_something_to_tm(*tm_ptr);

// Steal the resource
std::unique_ptr<TmContainer> other_tm_ptr = std::move(tm_ptr);

// Replace the managed object with another one
tm_ptr = std::make_unique<TmContainer>;

我强烈反对这些做法,因为它们容易出错且可读性较差。如果您确实需要此功能,最好提供如下界面。

tm& time() { return *m_time; }

std::unique_ptr<tm> release_time() { return {std::move(m_time)}; }

// If tm is cheap to move
void set_time(tm t) { m_time = make_unique<tm>(std::move(t)); }

// If tm is not cheap to move or not moveable at all
void set_time(std::unique_ptr t_ptr) { m_time = std::move(t_ptr); }

【讨论】:

  • “窃取资源”隐式将资源替换为空资源,“替换资源”隐式窃取它(然后立即将其交给delete)。这些是相同的用例(但你关于它不好的观点是正确的)
  • 我不同意它们是同一个用例。一个使资源保持活动状态也使容器为空(在某些项目中这甚至可能不是有效状态),而另一个丢弃资源并使容器指向有效的东西。基本机制是相似的,但它们在逻辑上是不同的动作。请注意,在我的首选界面中,一个是release_time,另一个是set_time,以及如果使用它没有意义,接口如何提供其中一个而不是另一个。
  • 我不认为这是一个糟糕的设计,如果使用得当,它会非常强大。想想任何存储 unique_ptr 的 STL 容器,您在访问它们时都有一个参考。
  • 事实上,如果unique_ptr 包含null_ptr,则返回引用将导致问题。
【解决方案2】:

评论太长了。对于请求的用例,我没有一个好主意。我唯一想到的是实用程序库的一些中间件。

问题是您想要和需要建模什么。语义是什么。我知道返回引用没有任何用处。

您的示例中唯一的优点是没有显式析构函数。如果您想制作此代码的精确镜像,恕我直言,它将是一个原始指针。应该使用什么确切类型取决于他们想要建模的确切语义。

也许您显示的代码背后的意图是返回一个非拥有指针,惯用(据我所知)它是通过原始指针建模的。如果您需要保证对象是活动的,那么您应该使用shared_ptr,但请记住,这意味着共享所有权——即将它的生命周期与TmContianer 分离。

使用原始指针会使您的代码同样失败,但有人可能会争辩说,也没有理由显式使用delete,并且可以通过范围正确管理对象的生命周期。

这当然是值得商榷的,就像单词和短语的语义和意义一样,但我的经验表明,c++ 人们就是这样写、说和理解指针的。

【讨论】:

    【解决方案3】:

    std::unique_ptr 按照设计不满足 CopyConstructibleCopyAssignable 的要求。

    因此,如果需要,必须将对象作为引用返回。

    【讨论】:

    • 问题是,何时需要。它也不必作为参考返回。一个经典的反例是按值返回一个临时的unique_ptr,或者通过std::move,这意味着放弃所有权。我认为任何使用智能指针的make_resource 实现都是这样工作的。
    • @luk32 是的,我同意。但我觉得,这不仅仅是需要,设计强迫个人使用引用返回。我对您的评论有疑问,是否可以按值返回临时的 unique_ptr ?按值返回需要复制操作rt。
    • 从 c++17 开始,如果临时文件未命名,则可以保证复制省略,并且在它被隐式移动之前。请参阅how it works,它甚至可以编译为c++11。在方法中创建资源并传递它的所有权,我认为您可以使用显式 std::move 来做到这一点,并拥有按值返回 unique_ptr 的类。这意味着无论你得到什么都属于类,现在它是调用者。这样一个对象就可以提供它的资源。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-29
    • 2022-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-28
    相关资源
    最近更新 更多