Justin Albano
Justin Albano

Reputation: 3949

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

While working on a project, I came across an interesting problem when passing an object into another object through its constructor when the object passed in is guaranteed to outlive (in terms of memory lifetime) the recipient object. Please bare in mind that I am still learning the ins-and-outs of C++11/C++14, so I am looking for constructive discussion that will help in my understanding of memory management and lifetimes with C++11/C++14-style semantics.

The setup for this question is as follows:

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;
};

Although a valid alternative to this setup would be to pass the TopLevelClass as an argument into the call method of the Context class, this is not possible in the scenario I am illustrating: The client code with access to a Context object may not have access to the TopLevelClass object.

While the code illustrated above is functionality correct for my needs, I feel as though there exists a code smell. Namely, storing a handle to the TopLevelClass object as a raw pointer does not convey the fact that the Context class is not responsible for managing the lifetime of this pointer (since, in this case, the TopLevelClass is guaranteed to outlive any Context object). Additionally, with use of C++11, I am hesitant to use a raw pointer, rather than a smart pointer (as per Scott Meyer's suggestion in Effective Modern C++).

One alternative I explored is to pass in the handle to the TopLevelClass using a shared pointer and storing this handle within the Context class as a shared pointer. This requires that the TopLevelClass inherit from std::enabled_shared_from_this in the following manner:

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;
};

The downside to this approach is that unless a std::shared_ptr exists for TopLevelClass a priori, then a std::bad_weak_ptr exception will be thrown (for more information, see this post). Since, in my case, there is no std::shared_ptr<TopLevelClass> created in the code, I cannot use the std::enable_shared_from_this<T> approach: I am restricted to returning a single instance of TopLevelClass using a static raw pointer, as per the requirements of my project, as follows

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

Is there an approach that exists that conveys the fact that Context is not responsible for managing its handle to the TopLevelClass instance, since the TopLevelClass will be guaranteed to outlive any Context object? I am also open to suggestions about changing the design that will side-skirt the problem altogether, so long as the design change does not overly complicate the simplicity of the design above (i.e., creating many different classes in order to get around simply passing a single pointer into the constructor of Context).

Thank you for your help.

Upvotes: 3

Views: 538

Answers (2)

villintehaspam
villintehaspam

Reputation: 8734

Passing in a raw pointer the way you are doing absolutely should mean that no ownership is being transferred.

If you have heard somone saying "don't use raw pointers" you probably missed part of the sentence - it should be "don't use owning raw pointers", i.e. there should not be a place somewhere where you have a raw pointer that you need to call delete on. Except possibly in some low level code. There is absolutely nothing wrong with just passing in pointers if you know for a fact that the object being pointed to outlives the object getting the pointer.

You are saying "Namely, storing a handle to the TopLevelClass object as a raw pointer does not convey the fact that the Context class is not responsible for managing the lifetime of this pointer". On the contrary, storing a raw pointer means exactly this - "This object does not manage the lifetime of the object pointed to by this pointer". In C++98 style code it did not necessarily mean that though.

An alternative to using a pointer is to use a reference. There are some caveats to this though, as you must initialize it in the constructor for instance and it cannot be set to nullptr like a pointer (which can also be a good thing). 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;
};

Here are some articles on the topic:

The C++ Core Guidelines:

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

Some earlier articles by 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/

There are probably also lots of videos from CppCon as well as Cpp and Beyond, but I was a bit too lazy to google up the appropriate ones.

Upvotes: 3

Chris Hayden
Chris Hayden

Reputation: 1144

One option is to use a type definition to convey non-ownership:

#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);
  }
};

This cleanly expressed the intent, though _tlc is still convertible directly to a raw pointer. We could make an actual class called borrowed_ptr (similar to shared_ptr) that better hides the raw pointer, but it seems like overkill in this case.

Upvotes: 1

Related Questions