C++
How do I call stdmakeshared on a class with only protected or private constructors
Creating shared pointers using std::make_shared is generally the preferred method for managing dynamically allocated objects in modern C++. However, a common challenge arises when dealing with classes that deliberately restrict constructor access, using either protected or private constructors. This design pattern is often employed to enforce specific creation pathways, such as the Factory pattern, or to control object instantiation for singleton-like behavior. So, how do I call std::make_shared on a class with only protected or private constructors? The direct answer is that you can’t, not without a workaround. The standard std::make_shared function requires public access to the class’s constructor. But fear not, there are several elegant solutions that enable you to combine the benefits of controlled object creation with the efficiency and exception safety of std::make_shared.
Understanding the Constructor Access Problem
The core issue stems from the design of std::make_shared itself. It directly invokes the class’s constructor to create the object within the allocated memory. When a constructor is declared protected, only the class itself, its derived classes, and its friends can access it. A private constructor is even more restrictive, limiting access solely to the class itself and its friends. The standard std::make_shared, without modification, does not fall into any of these categories when used outside the class definition. Therefore, a compilation error occurs when you attempt to use std::make_shared directly on a class with restricted constructors.
Consider a simple example: suppose you have a class named MyClass with a private constructor. Attempting to create a std::shared_ptrstd::make_shared<myclass>()</myclass> would fail. This is because std::make_shared tries to directly call the private constructor from outside the class, which is not permitted by the language’s access control rules. This limitation, however, encourages better design patterns such as the Factory pattern which encapsulate object creation and provide an abstraction layer.
The inability to directly use std::make_shared in these scenarios can seem limiting, but it’s a deliberate feature of C++’s access control system. This system is designed to enforce encapsulation and control how objects are created and managed. The goal is to prevent unintended or improper object instantiation, leading to more robust and maintainable code. The challenge then becomes finding alternative approaches that respect these design principles while still leveraging the advantages of shared pointers.
The “Friend” Function Approach
One of the most common and straightforward solutions is to declare std::make_shared as a friend function of the class. By doing so, you grant std::make_shared access to the class’s protected or private constructors. This allows std::make_shared to create the object directly, bypassing the access restrictions that would otherwise prevent instantiation. This method provides a clean and efficient way to utilize shared pointers with classes that have restricted constructors.
To implement this, you simply add a friend declaration within the class definition. For example: cpp class MyClass { private: MyClass() {} // Private constructor friend std::shared_ptrstd::make_shared<myclass></myclass> has special access privileges to the private members of MyClass, including its constructor. Now, you can successfully use std::make_shared<myclass>()</myclass> to create a shared pointer to MyClass. Declaring std::make_shared as a friend requires careful consideration. You should only grant friendship to functions that you trust and that need access to the class’s private members. Overuse of friend declarations can weaken encapsulation and make the code harder to maintain. The use of friend functions is a powerful mechanism, but should be wielded judiciously to maintain the integrity of the class’s design.
This approach is often preferred because it preserves the exception safety and efficiency benefits of std::make_shared. According to Herb Sutter, a leading C++ expert, “Prefer using make_shared and allocate_shared, and avoid naked new as much as possible.” [1] By making std::make_shared a friend, you’re able to follow this guideline even when dealing with classes that have restricted constructors.
The Factory Function Pattern
The Factory pattern is a creational design pattern that provides an interface for creating objects without specifying their concrete classes. It involves defining a separate function or class (the “factory”) responsible for object instantiation. This approach is particularly useful when you want to control the creation process, encapsulate complex initialization logic, or abstract away the specific types of objects being created. Using a Factory function allows you to bypass the access restrictions of protected or private constructors while still utilizing std::make_shared for memory management.
Here’s how you can implement the Factory pattern in conjunction with std::make_shared: cpp class MyClass { protected: MyClass() {} // Protected constructor public: static std::shared_ptrstd::make_shared<myclass>()</myclass> is called to create the object, and the resulting shared pointer is returned. Since the Create() function is a member of MyClass, it has access to the protected constructor. Clients of MyClass can then use the Create() function to obtain a shared pointer to MyClass without directly calling the constructor.
This approach offers several advantages. It encapsulates the object creation logic within the class, providing a clear and controlled interface for instantiation. It allows you to perform additional initialization or configuration steps within the Create() function before returning the shared pointer. It also decouples the client code from the specific details of how MyClass is created, making the code more flexible and maintainable. The Factory pattern is a powerful tool for managing object creation and ensuring proper initialization.
Custom Deleters and Placement New (Advanced)
While less common, custom deleters and placement new offer more advanced solutions. Custom deleters allow you to specify a function or function object that will be called when the std::shared_ptr is destroyed. This can be useful if you need to perform custom cleanup operations or release resources that are not automatically managed by the class’s destructor. Placement new, on the other hand, allows you to construct an object in a pre-allocated memory buffer. This can be used to create the object with a protected or private constructor in a separate step and then wrap it in a std::shared_ptr.
To use a custom deleter, you can pass a function or function object to the std::shared_ptr constructor. For example: cpp void MyDeleter(MyClass ptr) { // Custom cleanup logic delete ptr; } std::shared_ptrstd::make_shared and is not recommended unless absolutely necessary.
Placement new can be used in conjunction with custom allocation to create objects with restricted constructors. This involves allocating a memory buffer, constructing the object in the buffer using placement new, and then creating a std::shared_ptr that owns the buffer. This approach is more complex than the other methods, but it can be useful in certain situations. Remember to always consider the alternatives before resorting to these more complex methods. Often, the Factory pattern or friend functions will provide a simpler and more maintainable solution.
FAQ: Calling std::make_shared with Restricted Constructors
- **Q: Why can't I directly use `std::make_shared` with a class that has a private constructor?**
- A: `std::make_shared` attempts to directly call the class's constructor from outside the class. A private constructor is only accessible from within the class itself or by its friends.
- **Q: What are the benefits of using `std::make_shared` over `new`?**
- A: `std::make_shared` provides exception safety and can improve performance by allocating the object and the shared pointer's control block in a single memory allocation. [\[2\]](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared)
- **Q: When should I use the Factory pattern instead of making `std::make_shared` a friend?**
- A: Use the Factory pattern when you need more control over the object creation process, such as performing complex initialization or abstracting away the specific types of objects being created. If you only need to bypass the constructor's access restriction, making `std::make_shared` a friend might be simpler.
- **Q: Are there any risks associated with making `std::make_shared` a friend?**
- A: Overuse of friend declarations can weaken encapsulation. Only grant friendship to functions that you trust and that need access to the class's private members.
- Step 1: Identify if the class has a
protectedorprivateconstructor. - Step 2: Determine if you need full control over object creation or just access to the constructor.
- Step 3: Implement either the “friend” function or the Factory pattern based on your needs.
The challenge of calling std::make_shared on classes with restricted constructors highlights the importance of understanding C++’s access control mechanisms and design patterns. By using the “friend” function approach or the Factory pattern, you can effectively combine controlled object creation with the benefits of shared pointers. The best approach depends on the specific requirements of your code and the level of control you need over the object creation process. Remember to prioritize encapsulation and maintainability while ensuring exception safety and efficiency. Explore further insights on object management in C++ here.
Ultimately, the key is to choose the solution that best balances your design goals with the need for efficient and safe memory management. Consider the trade-offs between simplicity, control, and encapsulation when selecting the appropriate approach. By carefully considering these factors, you can effectively leverage shared pointers even in the presence of restricted constructors. Want to learn more about advanced C++ memory management techniques? Check out related articles on smart pointers and RAII principles. [3]
Question & Answer :
I have this code that doesn’t work, but I think the intent is clear:
testmakeshared.cpp
#include <memory> class A { public: static ::std::shared_ptr<A> create() { return ::std::make_shared<A>(); } protected: A() {} A(const A &) = delete; const A &operator =(const A &) = delete; }; ::std::shared_ptr<A> foo() { return A::create(); }
But I get this error when I compile it:
g++ -std=c++0x -march=native -mtune=native -O3 -Wall testmakeshared.cpp In file included from /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:52:0, from /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/memory:86, from testmakeshared.cpp:1: testmakeshared.cpp: In constructor ‘std::_Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp>::_Sp_counted_ptr_inplace(_Alloc) [with _Tp = A, _Alloc = std::allocator<A>, __gnu_cxx::_Lock_policy _Lp = (__gnu_cxx::_Lock_policy)2u]’: /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr_base.h:518:8: instantiated from ‘std::__shared_count<_Lp>::__shared_count(std::_Sp_make_shared_tag, _Tp*, const _Alloc&, _Args&& ...) [with _Tp = A, _Alloc = std::allocator<A>, _Args = {}, __gnu_cxx::_Lock_policy _Lp = (__gnu_cxx::_Lock_policy)2u]’ /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr_base.h:986:35: instantiated from ‘std::__shared_ptr<_Tp, _Lp>::__shared_ptr(std::_Sp_make_shared_tag, const _Alloc&, _Args&& ...) [with _Alloc = std::allocator<A>, _Args = {}, _Tp = A, __gnu_cxx::_Lock_policy _Lp = (__gnu_cxx::_Lock_policy)2u]’ /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:313:64: instantiated from ‘std::shared_ptr<_Tp>::shared_ptr(std::_Sp_make_shared_tag, const _Alloc&, _Args&& ...) [with _Alloc = std::allocator<A>, _Args = {}, _Tp = A]’ /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:531:39: instantiated from ‘std::shared_ptr<_Tp> std::allocate_shared(const _Alloc&, _Args&& ...) [with _Tp = A, _Alloc = std::allocator<A>, _Args = {}]’ /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr.h:547:42: instantiated from ‘std::shared_ptr<_Tp1> std::make_shared(_Args&& ...) [with _Tp = A, _Args = {}]’ testmakeshared.cpp:6:40: instantiated from here testmakeshared.cpp:10:8: error: ‘A::A()’ is protected /usr/lib/gcc/x86_64-redhat-linux/4.6.1/../../../../include/c++/4.6.1/bits/shared_ptr_base.h:400:2: error: within this context Compilation exited abnormally with code 1 at Tue Nov 15 07:32:58
This message is basically saying that some random method way down in the template instantiation stack from ::std::make_shared can’t access the constructor because it’s protected.
But I really want to use both ::std::make_shared and prevent anybody from making an object of this class that isn’t pointed at by a ::std::shared_ptr. Is there any way to accomplish this?
This answer is probably better, and the one I’ll likely accept. But I also came up with a method that’s uglier, but does still let everything still be inline and doesn’t require a derived class:
#include <memory> #include <string> class A { protected: struct this_is_private; public: explicit A(const this_is_private &) {} A(const this_is_private &, ::std::string, int) {} template <typename... T> static ::std::shared_ptr<A> create(T &&...args) { return ::std::make_shared<A>(this_is_private{0}, ::std::forward<T>(args)...); } protected: struct this_is_private { explicit this_is_private(int) {} }; A(const A &) = delete; const A &operator =(const A &) = delete; }; ::std::shared_ptr<A> foo() { return A::create(); } ::std::shared_ptr<A> bar() { return A::create("George", 5); } ::std::shared_ptr<A> errors() { ::std::shared_ptr<A> retval; // Each of these assignments to retval properly generates errors. retval = A::create("George"); retval = new A(A::this_is_private{0}); return ::std::move(retval); }
Edit 2017-01-06: I changed this to make it clear that this idea is clearly and simply extensible to constructors that take arguments because other people were providing answers along those lines and seemed confused about this.