Verify at compile time that objects are created as shared_ptr
Asked Answered
L

1

6

There are classes that I write (often as part of boost::asio) whose objects depend on being wrapped in a shared_ptr because they use shared_from_this(). Is there a way to prevent an object from being compiled if it's not instantiated in a shared_ptr?

So, what I'm looking for:

std::shared_ptr<MyClass> a = std::make_shared<MyClass>(); // should compile fine
std::unique_ptr<MyClass> a = std::make_unique<MyClass>(); // compile error
MyClass a; // compile error
Lanctot answered 27/12, 2018 at 19:34 Comment(7)
I second what LRiO said. In general, it is best (if possible) that objects are not self-aware if they are contained in a shared_ptr, or on the heap, or on the stack, or by pointer, or in a vector, or as a data member, or as a global. As soon as they are self-aware of how they are life-cycle managed, they become a lot more constrained. Unnecessarily so. shared_from_this is (arguably) an anti-pattern. But... sometimes it may be a necessary anti-pattern.Rowdy
Ouch, in particular, at not being able to use a unique pointer.Rozanne
You could initialize the owning smart to this in the ctor with a custom deleter and then arm it latter.Croaky
Is it a base class or a final class? Can it be used as a member of another object (of another, etc.), or an element of container, member of another object, as long as that object has dynamic lifetime and is managed by a smart pointer?Croaky
@TimRandall Not allowing the use of a unique smart ptr is a consequence of wanting to allow client to use weak ptr.Croaky
@Croaky There's no specific answer to your question because I was asking in general. I have all kinds of cases with me, and when I asked this question (days ago) I wanted to know what my options are and whether there's something I'm missing on how to make this cleaner.Lanctot
Another issue is whether constructors might throw lately: can you define a point in the body of MyClass::MyClass(...) where construction of the object is definitive, that is no other constructor of a super-object (here the difference between derived classes and classes having MyClass as member is insignificant) might throw.Croaky
T
13

Make its constructor private and give it a static factory member function that creates a shared_ptr. Don't forget to document your design decision in a comment!

// Thing that foos the bar
struct Foo : std::enable_shared_from_this<Foo>
{
   // Returns a shared_ptr referring to a new instance of Foo
   static std::shared_ptr<Foo> CreateShared()
   {
      return std::shared_ptr<Foo>(new Foo);
   }

private:
   // To avoid bugs due to the shared_from_this base,
   // we restrict Foo creation to being via CreateShared().
   Foo() = default;
};

(I can't imagine that std::make_shared would work due to the private ctor, but you can try it.)

I've got to say, though, this doesn't sound like the sort of thing a class should be taking responsibility for. It's kind of programming backwards.

To steal Eljay's words:

In general, it is best (if possible) that objects are not self-aware if they are contained in a shared_ptr, or on the heap, or on the stack, or by pointer, or in a vector, or as a data member, or as a global. As soon as they are self-aware of how they are life-cycle managed, they become a lot more constrained. Unnecessarily so. shared_from_this is (arguably) an anti-pattern. But... sometimes it may be a necessary anti-pattern.

I would prefer avoiding enable_shared_from_this and letting people use your Foo however they see fit, such as through a nice lean unique_ptr.

Thatcher answered 27/12, 2018 at 19:37 Comment(7)
Related #8147527Afoul
If you want make shared, add a private token type struct private_ctor_t { explicit private_ctor_t(int){} }; and a public ctor that takes private_ctor_t as its first argument. Then call make_shared<Foo>(private_ctor_t(0)).Pen
@Yakk-AdamNevraumont Suppose. Meh though. Seems a bit leaky. Since the effect of not being able to do make_unique is limited to the CreateShared impl I think I prefer it how it is tbhThatcher
@light you misunderstand I think; the private ctor type is private. Only way to call the public ctor is with the private ctor type instance. Only way to get that is to be a friend, member or have it passed to you. Basically it permits passing the token to make shared in create shared, saving a memory allocation and improving locality. This is about make_shared, not make_unique.Pen
@Yakk-AdamNevraumont Oh, right, that's better than I thought. Still feels a bit leaky with the public ctor taking something you can't create yourself, but it's acceptable to me ^_^ Does it it improve locality though? All it changes is the method of construction, and in a case like this (built-in type args) does that even really serve a purpose?Thatcher
@light make shared means the addref on the shared ptr is in memory adjacent to the object; and as shared ptrs are often copied prior to being used, this is better memory locality than them being in completely unrelated locations.Pen
@Yakk-AdamNevraumont I had no idea make_shared did that. Always thought it was just a way to emplace ctor args and avoid exception-safety issues(ish). Nais. In that case, we should definitely make this class make_shared-capable.Thatcher

© 2022 - 2024 — McMap. All rights reserved.