当一个对象被保证比其包含的对象更长寿时,应该如何存储它

How should an object be stored when it is guaranteed to outlive its containing object?

本文关键字:何存储 存储 一个对象 对象 包含      更新时间:2023-10-16

在处理一个项目时,当通过一个对象的构造函数将一个对象传递到另一个对象时,我遇到了一个有趣的问题,因为传入的对象保证比接收方对象寿命长(就内存寿命而言)。请记住,我仍在学习C++11/C++14的来龙去脉,因此我正在寻找建设性的讨论,这将有助于我理解C++11/C++14风格语义的内存管理和寿命。

这个问题的设置如下:

class TopLevelClass {
public:
    void someMethod (int someValue) {
        // Do some work
    }
    std::unique_ptr<Context> getContext () {
        return std::make_unique<Context>(this);
    }
};
class Context {
public:
    Context (TopLevelClass* tlc) : _tlc(tlc) {}
    void call (int value) {
        // Perform some work and then call the top level class...
        _tlc->someMethod(value);
    }
protected:
    TopLevelClass* _tlc;
};

尽管此设置的有效替代方案是将TopLevelClass作为参数传递到Context类的call方法中,但在我所示的场景中这是不可能的:可以访问Context对象的客户端代码可能无法访问TopLevelClass对象。

虽然上面所示的代码的功能符合我的需求,但我觉得好像存在代码气味。也就是说,将TopLevelClass对象的句柄存储为原始指针并不能传达Context类不负责管理该指针的生存期的事实(因为在这种情况下,TopLevelClass被保证比任何Context对象都长寿)。此外,在使用C++11时,我不太愿意使用原始指针,而不是智能指针(根据Scott Meyer在Effective Modern C++中的建议)。

我探索的一种替代方案是使用共享指针将句柄传递给TopLevelClass,并将该句柄作为共享指针存储在Context类中。这要求TopLevelClass以以下方式从std::enabled_shared_from_this继承:

class TopLevelClass : public std::enable_shared_from_this<TopLevelClass> {
public:
    // Same "someMethod(int)" as before...
    std::unique_ptr<Context> getContext () {
        return std::make_unique<Context>(shared_from_this());
    }
};
class Context {
public:
    Context (std::shared_ptr<TopLevelClass> tlc) : _tlc(tlc) {}
    // Same "call(int)" as before...
protected:
    std::shared_ptr<TopLevelClass> _tlc;
};

这种方法的缺点是,除非TopLevelClass先验存在std::shared_ptr,否则将抛出std::bad_weak_ptr异常(有关更多信息,请参阅本文)。由于在我的情况下,代码中没有创建std::shared_ptr<TopLevelClass>,因此我不能使用std::enable_shared_from_this<T>方法:根据我的项目要求,我只能使用static原始指针返回TopLevelClass的单个实例,如下

static TopLevelClass* getTopLevelClass () {
    return new TopLevelClass();
}

有没有一种方法可以传达这样一个事实,即Context不负责管理其对TopLevelClass实例的句柄,因为TopLevelClass将保证比任何Context对象都要长?我也愿意接受关于更改设计的建议,只要设计更改不会使上述设计的简单性过于复杂(即,创建许多不同的类,以避免简单地将单个指针传递到Context的构造函数中),就可以完全绕过问题。

谢谢你的帮助。

以绝对的方式传递原始指针应该意味着没有所有权被转移。

如果你听到有人说"不要使用原始指针",你可能错过了句子的一部分——应该是"不要使用拥有原始指针的",也就是说,不应该有一个地方有你需要调用delete的原始指针。可能在一些低级别代码中除外。如果你知道被指向的对象比获得指针的对象更长寿,那么只传递指针绝对没有错。

您说的是"也就是说,将TopLevelClass对象的句柄存储为原始指针并不能传达Context类不负责管理该指针的生存期这一事实"恰恰相反,存储一个原始指针意味着——"此对象不管理此指针指向的对象的生存期"。在C++98风格的代码中,这并不一定意味着这一点。

使用指针的另一种选择是使用引用。不过,有一些注意事项,例如,您必须在构造函数中对其进行初始化,并且不能像指针一样将其设置为nullptr(这也是一件好事)。I.e:

class TopLevelClass {
public:
    void someMethod (int someValue) {
        // Do some work
    }
    std::unique_ptr<Context> getContext () {
        return std::make_unique<Context>(*this);
    }
};
class Context {
public:
  Context(TopLevelClass &tlc) : _tlc(tlc) {}
  void call (int value) {
    // Perform some work and then call the top level class...
    _tlc.someMethod(value);
  }
private:
  TopLevelClass &_tlc;
};

以下是一些关于这个主题的文章:

C++核心指南:

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rr-ptr

Herb Sutter的一些早期文章:

http://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/

http://herbsutter.com/2013/05/30/gotw-90-solution-factories/

http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/

http://herbsutter.com/elements-of-modern-c-style/

可能还有很多来自CppCon、Cpp和Beyond的视频,但我有点懒得在谷歌上搜索合适的视频。

一种选择是使用类型定义来传达非所有权:

#include <memory>
template<typename T>
using borrowed_ptr = T *;
class TopLevelClass;
class Context {
public:
  Context(borrowed_ptr<TopLevelClass> tlc)
    : _tlc(std::move(tlc))
  { }
private:
  borrowed_ptr<TopLevelClass> _tlc;
};
class TopLevelClass {
public:
  std::unique_ptr<Context> getContext() {
    return std::make_unique<Context>(this);
  }
};

这清楚地表达了意图,尽管_tlc仍然可以直接转换为原始指针。我们可以制作一个名为borrowed_ptr(类似于shared_ptr)的实际类,它可以更好地隐藏原始指针,但在这种情况下,这似乎有些过头了。