Reputation: 3949
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
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
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