My apologies, while reading your post I had the impression of a seasoned engineer so I chose a terse, dense reply. In reality most of us are self taught no matter what degrees we got, with the possible exception of the most recent graduates, and their days as autodidacts are fast upon them, too.
What is more formally called "in place syntax" applies to "new" declarations.
For any type T, new T() creates an object from the heap, calling a construct and optionally taking parameters.
Given a raw block of memory, say void * v = malloc( 1024 ):
::new( v ) T();
This calls the constructor for T, having placed the object at the location v, without other allocation.
This technique is used in the make_shared approach for shared_ptr. That would be used thus:
std::shared_ptr<T> p = std::make_shared<T>();
Where optional parameters for T's constructor are placed typically inside the parenthesis. This fashions a new T inside a node "owned" by p. Subsequently p is used as any raw pointer for access, as in p->x. When p falls from scope, deletion is automatic. However, the object T is constructed in place, not by a separate "new T()", but by "new(v) T()", where v is position immediately after the node structure, itself allocated to accommodate all the space required by T. This creates one allocation for two C++ structures.
Subsequently, the destructor can be called explicitly, assuming rp is a raw pointer of type T, with:
rp->~T();
While this calls the destructor, it takes no action with regard to deallocating storage. That would be handled separately.
In modern C++ there is a paradigm of exclusive use of either shared_ptr, unique_ptr or scoped_ptr instead of raw pointers. As such, there is seldom any use for an explicit call to delete. Despite copious tutorials invoking the standard use of "new" and "delete", that usage is consider an older style more associated with pre-C++11. There is virtually zero overhead using unique_ptr or scoped_ptr, as they are uncommonly lightweight considering their function. They have certain restrictions appropriate for their use cases (unique_ptr can be moved but not copied).
std::unique_ptr<T> p = make_unique<T>();
Is effectively the same as:
T * p = new T();
With the exception of the exact type of p. p->x or p->foo() is the same for either case. The main difference is there is no use for "delete p;" in the first case, because unique_ptr<T> performs destruction upon release from scope.
std::scoped_ptr is nearly identical in most respects, but does not allow moves or copies (it guards its charge very jealously).
std::shared_ptr functions similarly with the expanded feature of reference counted ownership. It is particularly suited for threaded design where "ownership" is not explicitly known. That is:
1 2 3
|
std::shared_ptr<T> p = make_shared<T>();
std::shared_ptr<T> p2 = p;
std::shared_ptr<T> p3 = p;
| |
This creates what is conceptually three pointers to the same T. In reality, while p->x and p2->x and p3->x all point to the same instance of T, they exert "ownership" (deletion, ultimately) through a node structure which counts how many of these shared pointers are "holding" that instance. It will not matter when any of these pointers fall from scope, even from other threads. The underlying instance will be maintained until the last of these fall from scope. Deletion is assured at that point.
At one point in early versions of shared_ptr, it was compatible with the standard use of new. This was valid in older versions from boost (from which shared_ptr was taken), so this was valid:
std::shared_ptr<T> p = new T();
Modern compilers generally don't allow this anymore without configuration. For a while (through the pre-C++11 era), make_shared was considered an optional performance enhancement. The source can still be examined in the current boost source for it.
If you have no interest in shared containment (it's slower), an all inline smart pointer is so lightweight as to nearly evaporate into nothing but awareness and automatic deletion.
A naive example would be:
1 2 3 4 5 6 7 8 9 10
|
template <typename T> spointer
{
private:
T * p;
public:
spointer( T *i ) : p( i ){};
~spointer() { if ( i ) delete i; }
T* operator ->() { return p; }
};
| |
It's major failing is ignoring assignment or copy construction. Alexandrescu would caution against any named member functions. There is no "get" function to get the pointer, only the -> operator. The std::scoped_ptr denies copies or assignments, while unique_ptr offers to move them, leaving the source "empty" (nullptr). This is necessary to avoid deleting the underlying instance owned by the smartpointer multiple times (crashing). std::shared_ptr offers copies through reference counting. Alexendrescu offers a good treatise in his 2001 book, "Modern C++", on the notion of a conversion operator to T *, so approach with caution. It is an option, but with thorns. In reality all copying of pointers, even in C, is fraught with peril. Ownership is unclear without serious attempt at clarity. That is the origin of reference counting. Deep copy, creating a duplicate instance, is an alternative, but unattractive.
If you contemplate the in place construction and explicit call of the destructor from an RAII manager (that is, a smart pointer), you should come to the realization that these techniques impose virtually zero demands upon the target classes they manage. If "new" and "delete" work on a struct or class, these techniques are generally applicable without alteration to the classes.
Since such a "smart pointer" is fully aware of the type (it's size, etc), you are in complete control of customized allocation/deallocation strategy.
I have not reviewed the current versions of shared_ptr to verify this issue. The last I worked on this in earnest was when I created a smart pointer library now used in high performance real time application work in the pre-C++11 era. However, the reason I wrote a new smart pointer in the first place was the discovery that shared_ptr could not be used with genuinely custom allocation strategies. It was an insidious problem, because 99% of the work was completed in my case, using the parameter clearly named as if custom allocation were possible. The problem wasn't allocation, though, it was deletion. For whatever reason, the original shared_ptr from that era would not support the notion of custom deletion. To use shared_ptr, I would have to alter the source. Since that was from boost at the time, it was an option to explore, but ultimately the design requirements included new features not found in shared_ptr, so a new smart pointer was created, including one similar to unique_ptr (before even auto_ptr existed).
That issue may have been resolved in recent versions of shared pointer - research would likely say by searching for custom allocation articles/papers regarding std::shared_ptr in modern C++.
If you want some research, a work by Alexandrescu from 2001 discusses a policy based smart pointer design called loki (or the loki library). Although it is older, the code and work is still quite relevant on the subject, particularly with advice on how to create the interface for smart pointers. It does not, however, include variadic templates, but that is actually a simple read to catch up and is pertinent for creating a counterpart to make_shared (it nearly becomes trivial, you'll need maybe an hour, if that, to read and then implement the techniques).
The CRTP you reference is also discussed in Alexndrescu's book, but policy based design is a related notion. Instead of base definition of CRTP, policy based templates simply inherit from the parameter, as in:
1 2 3 4 5 6
|
template< T, B > class A : public B
{
T v;
};
| |
B is called a policy.
If B is similarly declared, it may also have a policy (or policies), going as deep as required.
Loki constructs smart pointers using policy based design in order to select behavior as required.