3

I know I can prevent ordinary heap allocation of custom class and its descendants by making the class's operator new private, but is there any way to prevent a user of a library from calling std::make_shared on a custom class (or its descendants)? Apparently, simply making the operator new private in a class does not stop it.

Note that I do not want to completely prevent shared pointers from being created by any means, as I intend to still be able to produce a std::shared_ptr for my custom class by calling a static generator method, but I still want to prevent others from trying to call std::make_shared on my class directly.

EDIT:

To address queries below, my ultimate goal here is a flexible and reusable mechanism for extracting a shared_ptr from a raw pointer. std::enable_shared_from_this is regretfully not very friendly when it comes to inheritance and especially multiple inheritance.

Also, because I am intending for this to be used as in a template class using CRTP, it would complicate matters somewhat for the a child class T to need to explicitly make the templated parent class it inherits from a friend so that it can access otherwise private constructors.

markt1964
  • 2,638
  • 2
  • 22
  • 54
  • 8
    This is a weird design choice. Why do you care if the shared pointer is created for the class or not? Class should not be in a business of their own storage. Perhaps if you can explain your ultimate goal, a better solution can be suggested. – SergeyA Sep 19 '19 at 17:15
  • 1
    if only the static generator should create instances then why not private constructor and befriend the generator? – 463035818_is_not_an_ai Sep 19 '19 at 17:17
  • 1
    *"I can prevent ordinary heap allocation of custom class ... by making the class's operator new private"* Can't one circumvent that with `::new`? – HolyBlackCat Sep 19 '19 at 17:17
  • Aren't you allowed to overload the function and mark it as deleted? – JVApen Sep 19 '19 at 18:25
  • @JVApen How would I do that for a template class without disabling std::make_shared for everything? Also, I still want to be able to call std::make_shared within the generator function. – markt1964 Sep 19 '19 at 18:29
  • Specialization? – JVApen Sep 19 '19 at 18:30
  • But how do you specialize for a class whose name you don't know yet? I want to disallow std::make_shared when N inherits from my custom template class that takes N as a type argument, using a CRTP. – markt1964 Sep 19 '19 at 18:32
  • @markt1964: FYI: "*my ultimate goal here is a flexible and reusable mechanism for extracting a shared_ptr from a raw pointer*" If that is an operation which you do so often that you need a "flexible and reusable mechanism" for it, *especially* one more powerful than the standard mechanism, then your design is not in a good place. Code which randomly claims ownership of an object which they were *not initially given ownership over* should usually be considered a code smell. Not necessarily wrong, just not good. – Nicol Bolas Sep 19 '19 at 18:59
  • No more smell than what is produced by `std::enable_shared_from_this` in the first place. And, as I said, that particular mechanism by itself is not ideally suited for cases where you may have a complex inheritance tree, and shared pointers might be used at any and every level. – markt1964 Sep 19 '19 at 19:05
  • @markt1964: "*No more smell than what is produced by `std::enable_shared_from_this` in the first place.*" Yes. Which is why the use of that mechanism should be strictly controlled. It's to be used where *essential*, not by default. Using it in a CRTP base class, for example, is *absolutely* the wrong way to go. A CRTP base class has no right to claim ownership of the derived instance, let alone to give that ownership to others. "*shared pointers might be used at any and every level*" That sounds like you're treating shared pointers like C++ garbage collection, when they very much are not. – Nicol Bolas Sep 19 '19 at 19:09
  • @NicolBolas If `std::enable_shared_from_this were` more friendly to inheritance, and particularly multiple inheritance, I'd agree. The problem is that if something is to inherit from `std::enable_shared_from_this`, it needs to happen exactly once on the inheritance tree, and this can be hard to control when the inheritance tree gets too complicated. The mechanism I have coded so far works, but does not currently prevent a user from calling `std::make_shared` on a class directly, which will break the assumption my code makes that shared pointers are generated by the generator function. – markt1964 Sep 19 '19 at 19:20
  • @markt1964 `enable_shared_from_this` is fundamentally a hack, and a very dirty one at that. It works only with SI, public (even though the class might consider it an implementation detail) and doesn't work for a member subobject. – curiousguy Sep 19 '19 at 22:06

3 Answers3

6

This is not specific for std::shared_ptr, but you can make the constructor private and that way force all instances of the class to get generated from your static method.

#include <iostream>
#include <memory>

class Foo {
    private:
    Foo() = default;

    public:
    static std::shared_ptr<Foo> make() {
        return std::shared_ptr<Foo>(new Foo);
    }
};

int main() {
    //Foo f1;
    //auto f2 = std::make_shared<Foo>(); 
    //above does not work since the constructor is private
    auto h = Foo::make();
}

You can also use deduplicators suggestion and use a private key to make the constructor inaccessible outside of the class.

#include <iostream>
#include <memory>

class Foo {
    private:
    struct FooKey {};

    public:
    Foo(FooKey) {};

    static std::shared_ptr<Foo> make() {
        return std::make_shared<Foo>(FooKey{});
    }
};

int main() {
    //Foo f1{Foo::FooKey{}};
    //auto f2 = std::make_shared<Foo>(Foo::FooKey{}); 
    //above does not work since Foo::FooKey is private
    auto h = Foo::make();
}
super
  • 12,335
  • 2
  • 19
  • 29
  • @GuillaumeRacicot Type mismatch. – Deduplicator Sep 19 '19 at 17:31
  • Better use an empty trivial type as a key, so `std::make_shared()` can be used. – Deduplicator Sep 19 '19 at 17:33
  • 1
    The existence of placement `new` means that controlling access to the constructor (this solution) is probably the only way to reliably take control of how and when your instance is created. – François Andrieux Sep 19 '19 at 17:41
  • @Deduplicator That's an option. Added since the question mentions `make_shared`. – super Sep 19 '19 at 17:41
  • 1
    This is interesting, but not readily estensible if I want to also inherit from the class. The child class will need to always know to call the parent class with an otherwise private (or protected) argument, and if other constructors are needed, that extra arg will have to prefix every one of them. It will, I'm afraid, start to look ugly after more than 3 or 4 classes are created. – markt1964 Sep 19 '19 at 18:35
2

Blocking user heap allocation of a type T is not actually a thing you can do in C++. At least, not so long as they can create Ts. Even if you manage to forbid make_shared<T>, your user can still do this:

unique_ptr opt = new optional<T>;
opt->emplace(...);

The T inside of *opt is definitely on the heap. Any number of other similar gymnastics can achieve the same effect. You can even call make_shared<optional<T>>, with the in_place parameters.

So long as T has publicly accessible constructors, or there is any publicly accessible way of constructing a T that returns a prvalue of it, users can find ways to store that T on the heap.

So the only way to prevent this is to make all of your constructors private (whether directly with private or a private key type or whatever other mechanism you want) and only provide publicly accessible functions that return shared_ptr<T>s.

Outside of macros, there is no C++ mechanism that just causes a type to work this way. It must be done individually, for each type that you want to work this way.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
0

Above answers are better solutions to the problem. But, if you're trying to not let the make_shared make new instance of your class, maybe this is helpful.

#include <iostream>
#include <memory>
using namespace std;

class A {
private:
    void * operator new(size_t size) {}
};

template<> shared_ptr<A> make_shared() {
    cout << "this was called.\n";
    return nullptr;
}

int main() {
    A a;
    shared_ptr<A> p = make_shared<A>(a);
    shared_ptr<A> q = make_shared<A>();
    cout << "p: " << p << endl;
    cout << "q: " << q << endl;

    return 0;
}

'p' gets a object address while 'q' doesn't.

Note: requires -fpermissive option in the compiler.

Arlak
  • 11
  • 2