Original:https://www.fluentcpp.com/2018/12/25/free-ebook-smart-pointers/
One thing that quickly confuses your c++ code and hinders its readability is memory management.If you don't do it well, this can turn a simple logic into an inexpressive chaotic management and leave your code out of control of memory security.
To ensure that all object programming tasks correctly remove very low levels of abstraction, you want to keep these tasks away from your business logic (or any form of logic), since well-written code can basically be boiled down to respectful levels of abstraction.
Smart pointers can effectively handle this problem and free your code from tedious work.
This series of articles will show you how to use them to make your code more expressive and correct.
We'll delve into this topic in depth, because I want everyone to keep up with everything in this series, so there are no prerequisites, so let's start with the basics of smart pointers.
stack and heap
Like many other languages, c++ has several types of memory that correspond to different parts of physical memory.They are: static, stack, and stack.Static is a very rich topic and deserves to be honored, so here we only focus on stacks and stacks.
stack
int f(int a) { if(a > 0) { std::string s = "a positive number"; std::cout << s << "\n"; } } return a; }
Here A and s are stored on the stack.Technically, this means that a and s are stored side by side in memory because they are pushed onto stacks maintained by the compiler.
However, these problems are not very relevant to daily work.
There is one important, critical, or even basic thing to know about stacks.It is the basis for the rest of this series.The good news is, it's simple:
Objects allocated on the stack will be automatically destroyed when they exceed scope.
You can read this article a few more times. If you want, you can print it by stringing it on your forearm
Read this sentence aloud to your spouse in a t-shirt so that you can often think of it,
In c++, scopes are defined by a pair of parentheses ({and}), except for those used to initialize objects:
std::vector<int> v = {1, 2, 3}; // this is not a scope if (v.size() > 0) { // this is the beginning of a scope ... } // this is the end of a scope
There are three ways an object can go out of range:
Encountered the next right parenthesis (}),
Encountered a return statement,
Discard an exception in the current scope that was not caught in the current scope.
In the first code example, s is destroyed at the right parenthesis of the if statement, and a is destroyed at the return statement of the function.
heap
The heap is where dynamically allocated objects are stored, that is, the object is assigned a new call that returns a pointer:
int * pi = new int(42);
After the above statement, the pi points to an int object that is allocated on the heap.
Strictly speaking, newallocated memory is called idle storage.Heaps are memory allocated by malloc, calloc, and realloc, which are legacy parts of C and are usually no longer used in new code, and will be ignored in this article (but we will discuss them later in this series).But the term heap is so common in developer terms that it is used to discuss any dynamically allocated memory that I use here.
In any case, to destroy objects assigned by new, we must do this manually by calling delete
delete pi;
Contrary to the stack, objects allocated on the stack are not automatically destroyed.The advantage is that they stay longer than the end of the scope and do not produce any copies, except for the very inexpensive pointers.In addition, pointers allow objects to be manipulated in a polymorphic manner: a pointer to a base class can actually point to an object of any derived class.
But at the cost of this flexibility, it leaves developers responsible for deleting them.
Deleting objects on the heap is not a simple task: delete must be called once and only once to release heap-based objects.If the object is not called, it will not be freed and its memory space is not reusable - this is called a memory leak.On the other hand, multiple calls to delete for the same address can result in undefined behavior.
This is where the code gets confused and expressive (and sometimes even correct).In fact, to ensure that all objects are properly destroyed, bookkeeping ranges from simple deletions to complex marking systems when early returns occur.
In addition, some interfaces are ambiguous in terms of memory management.Consider the following example:
House* buildAHouse();
As the caller of this function, should I delete the pointer it returns? If I don't, and nobody does, it's a memory leak.But if I do, and others do, then this is undefined behavior.Between the Devil and the dark blue sea.
I think all this has led to a bad reputation for c++ as a complex language for memory management.
Fortunately, the smart pointer will handle all this for you.
RAII: the magic four letters
RAII is a very common concept in c++ that simplifies memory management of objects on the stack by using the basic properties of the stack (see your arm or upper body of your spouse).In fact, RAII can even be used to manage any type of resource conveniently and securely, not just memory.Oh, I won't write the meaning of these four letters because it doesn't matter to me and it's confusing.You can think of them as someone's name, like a c++ superhero
The principle of RAII is simple: encapsulate a resource (such as a pointer) into an object and release it in a destructor.This is what smart pointers do:
template <typename T> class SmartPointer { public: explicit SmartPointer(T* p) : p_(p) {} ~SmartPointer() { delete p_; } private: T* p_; };
The key is that you can manipulate the smart pointer as an object allocated on the stack.The compiler automatically calls the smart pointer destructor because...
Objects allocated on the stack will be automatically destroyed when they exceed scope.This calls delete on the wrapped pointer.For once.Simply put, smart pointers behave like pointers, but when they are destroyed, they delete the object they point to.
The code examples above are just for understanding RAII.But it is by no means a complete interface, a real-world smart pointer.
First, a smart pointer behaves like a pointer in many ways: it can be dereferenced by an operator or operator - >, that is, you can call sp or sp - > members on it.It can also be converted to bool, so it can be used in if statements like a pointer:
if(sp) { ... }
It tests the zero degree of the underlying pointer.Finally, the underlying pointer itself can be accessed using the.get() method.
Second, and perhaps more importantly, the interface above lacks one aspect: it does not handle replication! In fact, a replicated SmartPoint also replicates the underlying pointer, so there is a bug in the code below:
{ SmartPointer<int> sp1(new int(42)); SmartPointer<int> sp2 = sp1; // now both sp1 and sp2 point to // the same object } // sp1 and sp2 are both destroyed, the pointer is deleted twice!
In fact, it deletes the underlying object twice, resulting in undefined behavior.
So what about copies? These are different features of different types of smart pointers.
This allows you to code your intentions very precisely.Stay tuned, as this is what we'll see in the next section of this series.
unique_ptr , shared_ptr , weak_ptr ,scoped_ptr , raw pointers - Knowing your smart pointers
As we saw when we were discussing what smart pointers are, we must make some positive decisions about how to duplicate them.Otherwise, the default copy constructor may result in undefined behavior
It has been proved that there are several effective ways to do this, which results in a variety of smart pointers.It is important to understand the role of these smart pointers because they are the way designs are expressed in code, so you can also read the code to understand the design.
Here we see various types of pointers, which exist there, sorted approximately in decreasing order of usefulness (according to me):
std::unique_ptr
This is the smart pointer used by default when writing this article.It became the standard for c++ 11.
Unique_The semantics of PTR is that it is the only owner of memory resources.One std::unique_ptr will hold a pointer and delete it in its destructor (unless you customize it, this is the subject of another part of the series).
This allows you to express your intentions in the interface.Consider the following functions:
std::unique_ptr<House> buildAHouse();
It tells you that it gives you a pointer to the house that you are the owner of.Except for unique_returned by the functionNo one will delete this pointer except ptr.Now that you have ownership, this gives you confidence that you can freely modify the values that point to the object.
Notice std::unique_ptr is the preferred pointer returned from the factory function.In fact, on the basis of processing memory, std::unique_ptr packages a common pointer and is therefore compatible with polymorphisms.
But this also works in another way by passing an std::unique_ptr as parameter:
class House { public: House(std::unique_ptr<PileOfWood> wood); ...
In this case, the house is owned by'PileOfWood'.
Note, however, that even if you receive unique_ptr, and no one else can access the pointer.In fact, if another context is in unique_Keep a copy of the pointer in ptr, then use unique_Of course, modifying the object pointed to by the PTR object affects this other context.If you don't want this to happen, you can express it by using unique_ptr const:
std::unique_ptr<const House> buildAHouse(); // for some reason, I don't want you // to modify the house you're being passed
To ensure there is only one unique_ptr has memory resources and cannot copy std::unique_ptr.However, ownership can be from a unique_ptr transferred to another
(You can do this by adding unique_ptr moves to another unique_ptr to pass or return them from the function).
Move can return std::unique_through function valuePTR implementation, or explicit return in code:
std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2 = move(p1); // now p2 hold the resource // and p1 no longer hold anything
Raw pointers
"What?", you might think."We're talking about smart pointers. What is the original pointer doing here?"
Well, even though the original pointers are not smart pointers, they are not "dumb" pointers.In fact, there are legitimate reasons to use them, although they do not often occur.They share many similarities with references, but the latter should be preferred unless in some cases (but this is the subject of another article).
Now I just want to focus on what the original pointer and reference represent in the code: the original pointer and reference represent access to the object, but not ownership.In fact, this is the default way to pass objects to functions and methods:
void renderHouse(House const& house);
When you hold a unique_This is especially important when the PTR object is passed to the interface.You do not pass unique_ptr, instead of passing a reference to it, refers to the object pointed to:
std::unique_ptr<House> house = buildAHouse(); renderHouse(*house);
std::shared_ptr
shared_ptr entered the c++ 11 standard, but appeared in Boost before that.
A memory resource can be made up of several stds::shared_ptr is also held.shared_ptr maintains a count internally of how many of them have the same resources and deletes memory resources when the last one is destroyed.
Therefore, std::shared_ptr allows replication, but uses a reference counting mechanism to ensure that each resource is deleted once and only once.
At first glance, std::shared_ptr seems to be a magic bullet for memory management because it can be delivered and still maintains memory security.
But std::shared_ptr should not be used by default for the following reasons:
Contrast having multiple resources with one owner (e.g. std::unique_ptr) has a more complex system.
Separately having multiple resource holders makes thread security more difficult.
When an object is not shared within a domain but still appears as a "shared" in the code for technical reasons, it makes the code counterintuitive.
Due to the bookkeeping associated with reference counts, this can result in performance costs in terms of time and memory.
Use std::shared_A good example of PTR is when objects are shared in a domain.Then, use the shared pointer to reflect it in an expressive way.Generally, nodes in a graph are well represented as shared pointers because several nodes can hold references to another node.
std::weak_ptr
weak_ptr entered the language in c++ 11, but appeared in Boost before that.
weak_ptr s can be used with other std::shared_ptr s save references to shared objects together, but they do not increase the reference count.This means that if there is no more std::shared_ptr holds an object that will be deleted even though some weak pointers still point to it.
Therefore, a weak pointer needs to check that the object it points to is still alive.To do so, it must be copied to an std::shared_ptr:
void useMyWeakPointer(std::weak_ptr<int> wp) { if (std::shared_ptr<int> sp = wp.lock()) { // the resource is still here and can be used } else { // the resource is no longer here } }
A typical use case is to break shared_ptr circular reference.Consider the following code:
struct House { std::shared_ptr<House> neighbour; }; std::shared_ptr<House> house1 = std::make_shared<House>(); std::shared_ptr<House> house2 = std::make_shared<House>();; house1->neighbour = house2; house2->neighbour = house1;
weak_ptr can be used to maintain the cache.Data may or may not have been cleared from the cache, weak_The PTR will reference the data.
boost::scoped_ptr
scoped_ptr exists in Boost but is not included in the standard (at least in c++ 17).
It simply disables replication, or even the move construct.Therefore, it is the only owner of the resource and its ownership cannot be transferred.Therefore, scoped_ptr can only exist in...A range.Or as a data member of an object.Of course, as a smart pointer, it retains the advantage of removing the underlying pointer from the destructor.
How to implement the pimpl idiom by using unique_ptr
pimpl, which means Implement Pointer, is a widely used technique for reducing compilation dependencies.We'll see how to use smart pointers to implement pimpl idioms.
In fact, pimpl idioms have a pointer responsible for managing memory resources, so using smart pointers sounds like only logic, such as std::unique_ptr
Use std::unique_ptr management life cycle
Today, it is somewhat disturbing to leave an original pointer in c++ to manage your own resources.A natural practice is to use std::unique_PTR (or another smart pointer) replaces it.
In this way, the Fridge destructor doesn't need to do anything anymore, and we can have the compiler automatically generate it for us
Destructor visibility
There is a rule in c++ that deleting a pointer results in undefined behavior if:
#include <memory> class Fridge { public: Fridge(); void coolDown(); private: class FridgeImpl; std::unique_ptr<FridgeImpl> impl_; }; #include "Engine.h" #include "Fridge.h" class FridgeImpl { public: void coolDown() { /* ... */ } private: Engine engine_; }; Fridge::Fridge() : impl_(new FridgeImpl) {}
There is a rule in c++ that deleting a pointer results in undefined behavior if:
The pointer is of type void*or
The type pointed to is incomplete, that is, just forward declaration,
FridgeImpl is in the header file.
Solution:
Unique_if the definition of the type is visible before delete is calledPTR checks in its destructor.Therefore, if the type is only a forward declaration, the compilation is rejected and delete is called.
In fact, std::unique_ptr is not the only component that provides this check: Boost also recommends checked_The delete function and its siblings ensure that the delete call is well-formed.
Because we remove the declaration of a destructor from the Fridge class, the compiler takes over and defines it for us.However, compiler-generated methods are declared inline, so they are implemented directly in the header file.There, the type of FridgeImpl is incomplete.
Therefore, the error.
The solution is to prevent the compiler from doing this for us by declaring destructors.So change to the following form:
#include <memory> class Fridge { public: Fridge(); ~Fridge(); void coolDown(); private: class FridgeImpl; std::unique_ptr<FridgeImpl> impl_; };
We can still use the default implementation of compiler-generated destructors.But we need to put it in the implementation file, after definition
#include "Engine.h" #include "Fridge.h" class FridgeImpl { public: void coolDown() { /* ... */ } private: Engine engine_; }; Fridge::Fridge() : impl_(new FridgeImpl) {} Fridge::~Fridge() = default;
This program can be compiled, run, and worked.
There are many other important aspects to consider when implementing pimpl in c++.For this reason, I suggest you take a look at the dedicated section in Herb Sutter's exception c++.
How to Transfer unique_ptr s From a Set to Another Set
Transfer a std::unique_ptr to another std::unique_ptr is a simple thing:
std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2; p2 = std::move(p1); // the contents of p1 have been transferred to p2
The case: transfering sets of unique_ptrs
Let's first look at std::set of std::unique_What will PTR represent, and then see what happened when trying to transfer the contents of one collection to another.
unique_ptrs set: unique and polymorphic
We can use base classes polymorphically by using a handle (pointer or reference).To encapsulate memory management, we will use std::unique_ptr.
Now, if we want to implement a collection of several objects of Base, but it can be a collection of any derived class, we can use unique_Collection of PTR s.
Finally, we may want to prevent duplication of our collection.This is what std::set does.
Note that to implement this constraint, std::set needs a way to compare its objects.
Indeed, announce a group by:
std::set<std::unique_ptr<Base>>
Comparison between collection elements calls std::unique_The PTR operator <, which compares the memory addresses of its pointers.
In most cases, this is not what you want.When we think of "no repetition", it usually means "no logical repetition", for example: no two elements have the same value.Instead of "No two elements in memory are at the same address."
To avoid logical duplication, we need to call the operator < on Base (if it exists, you can use the id provided by the Base instance) to compare elements and determine if they are duplicates.In order for the set to use this operator, we need to customize the comparator of the set:
struct ComparePointee { template<typename T> bool operator()(std::unique_ptr<T> const& up1, std::unique_ptr<T> const& up2) { return *up1 < *up2; } }; std::set<std::unique_ptr<int>, ComparePointee> mySet;
To avoid writing this type every time you instantiate such a collection in your code, you can hide its technical behind an alias
template<typename T> using UniquePointerSet = std::set<std::unique_ptr<T>,ComparePointee>;
Transferring unique_ptrs between two sets
Well.We are ready and ready to move elements from one collection to another.Here are our two sets:
UniquePointerSet<Base> source; source.insert(std::make_unique<Derived>()); UniquePointerSet<Base> destination;
To transfer elements effectively, we use the insert method:destination.insert(begin(source), end(source));
But this will cause compilation errors!
In fact, the insert method attempts to copy unique_ptr element.
Then what shall I do?
C++17's new method on set: merge
Collections and mappings in c++ are internally implemented as trees.This ensures the algorithm complexity guaranteed by the method of their interface.It will not appear on the interface until c++ 17.
c++ 17 adds a merge method to the collection:
destination.merge(source);
This allows the destination to take over the nodes of the source internal tree.It's like splicing a list.Therefore, after executing this line, destination owns elements owned by source, and source is empty.
Unique_because only the node is modified, not the content inside the nodePTR s can't feel anything.They were not even moved.
Destination now has unique_ptr s, the end of the story.
If you don't produce c++ 17, as many people do when I write these lines, what can you do?
We can't move from a set
The standard algorithm for moving elements from one set to another is std::move.
Here's how it works with std::vector s
std::vector<std::unique_ptr<Base>> source; source.push_back(std::make_unique<Derived>()); std::vector<std::unique_ptr<Base>> destination; std::move(begin(source),end(source),std::back_inserter(destination));
After executing this line, destination owns elements owned by source, and source is not empty, but empty unique_ptr s.
Now let's do the same for our collection:
UniquePointerSet<Base> source; source.insert(std::make_unique<Derived>()); UniquePointerSet<Base> destination; std::move(begin(source), end(source), std::inserter(destination, end(destination)));
We get the same compilation errors at the beginning, some unique_ptrs are copied:
This may seem surprising.The purpose of the move algorithm is to avoid the unique_Copy on PTR elements, but move them, so why copy them?
The answer lies in how a collection provides access to its elements.The iterator of the set does not return unique_when dereferencingPTR &, instead of returning const unique_PTR &.It is designed to ensure that values within a collection are not modified without the collection being aware of it.In fact, it can break ranking invariants.
This is what happened:
< std:: dereference the iterator on move set and get const unique_PTR &,
It calls std:: Move the reference to get const unique_PTR &&,
It calls the insert method on the insert output iterator and passes const unique_PTR &&,
The insert method has two overloads: one uses const unique_Ptr&, the other uses unique_PTR &&.One is unique_PTR &&.Since the type we pass is const, the compiler cannot resolve the call to the second method, but instead calls the first method.
Then insert the insert overload on the output iterator call set using const unique_ptr &, then call unique_with a left-value referenceCopy constructor for ptr, which will result in compilation errors.
Making a sacrifice
So before c++ 17, it seemed impossible to move elements from a collection.Something must be abandoned: either moving or setting.This gives us two possible areas to give up.
Keeping the set but paying up for the copies
To discard movement and accept copying elements from one collection to another, we need to copy unique_Content pointed to by PTR s.
For this reason, let's assume that Base has is a polymorphic cloning method that is overridden in derivation:
class Base { public: virtual std::unique_ptr<Base> cloneBase() const = 0; // rest of Base... }; class Derived : public Base { public: std::unique_ptr<Base> cloneBase() const override { return std::make_unique<Derived>(*this); } // rest of Derived... };
At call site, we can put unique_ptrs are copied from one collection to another, for example:
auto clone = [](std::unique_ptr<Base> const& pointer){ return pointer->cloneBase(); }; std::transform(begin(source), end(source), std::inserter(destination, end(destination)), clone);
Or by loop:
for (auto const& pointer : source) { destination.insert(pointer->cloneBase()); }
Keeping the move and throwing away the set
The collection that does not allow movement is the source collection.If you only want the target to have a unique element, you can replace the source set with std:: vector.
In fact, std::vector s do not add constants to the values returned by their iterators.Therefore, we can move its elements from std::move algorithm:
std::vector<std::unique_ptr<Base>> source; source.push_back(std::make_unique<Derived>(42)); std::set<std::unique_ptr<Base>> destination; std::move(begin(source), end(source), std::inserter(destination, end(destination)));
Then, the target set contains a unique_ptr, which contains content previously in the source, and the source vector now contains an empty unique_ptr.
Live at head
As you can see, there are some ways to solve the problem of unique_ptr transfer from one set to another.But the real solution is the merge method of std::set in c++ 17.
As languages develop, standard libraries become better and better.Let's move as far as possible to the latest version of c++ and never look back.
Custom deleters
Let's take an example of a House class, which carries its build instructions. It is polymorphic, can be a sketch or a mature blueprint:
One way to handle instruction lifecycles is to treat them as unique_ptr is stored internally.Then say a copy of the house can copy the instructions:
class House { public: explicit House(std::unique_ptr<Instructions> instructions) : instructions_(std::move(instructions)) {} House(House const& other) : instructions_(other.instructions_->clone()) {} private: std::unique_ptr<Instructions> instructions_; };
In fact, an instruction has a polymorphic clone, which is implemented by a derived class:
{ public: virtual std::unique_ptr<Instructions> clone() const = 0; virtual ~Instructions(){}; }; class Sketch : public Instructions { public: std::unique_ptr<Instructions> clone() const { return std::unique_ptr<Instructions>(new Sketch(*this)); } }; class Blueprint : public Instructions { public: std::unique_ptr<Instructions> clone() const { return std::unique_ptr<Instructions>(new Blueprint(*this)); } };
Here's a way to build a house:
enum class BuildingMethod { fromSketch, fromBlueprint }; House buildAHouse(BuildingMethod method) { if (method == BuildingMethod::fromSketch) return House(std::unique_ptr<Instructions>(new Sketch)); if (method == BuildingMethod::fromBlueprint) return House(std::unique_ptr<Instructions>(new Blueprint)); throw InvalidBuildMethod(); }
The construction method may come from user input.
When an object can come from another memory source, the situation becomes more challenging, such as the stack:
Blueprint blueprint; House house(???); // how do I pass the blueprint to the house?
In fact, we can't make unique_ptr is bound to the stack allocated object because calling delete will result in undefined behavior.
One solution is to copy the blueprint and allocate it on the heap.This may be
Okay, or maybe it's expensive (I've had a similar situation before, and it used to be a bottleneck in this project).
However, passing objects allocated on the stack is a reasonable requirement.Then, when the object comes from the stack, we don't want the house to break the instructions in its destructor.
Std::unique_What can PTR do here?
Seeing the real face of std::unique_ptr
Most of the time, the c++ unique pointer is used as std::unique_ptr.But its full type has a second template parameter, its deleter:
template< typename T, typename Deleter = std::default_delete<T> > class unique_ptr;
std::default_delete is a function object that calls delete on call.However, it is only the default type of delete program and can be changed for a custom delete program.
This allows you to use a unique pointer for a type that has specific code to process its resources.This occurs in legacy code from C, where a function is usually responsible for releasing an object and its contents:
struct GizmoDeleter { void operator()(Gizmo* p) { oldFunctionThatDeallocatesAGizmo(p); } }; using GizmoUniquePtr = std::unique_ptr<Gizmo, GizmoDeleter>;
(By the way, as a step toward simplifying legacy code, you want it to work with std::unique_ptr compatible, this technique is very useful.)
Now with this feature, let's go back to the motivation scene.
Using several deleters
Our initial question was that we wanted unique_ptr deletes instructions unless they come from the stack, in which case we want them to be left alone.
Given the circumstances, you can customize the deletor to delete or not delete.To do this, we can use several delete functions, all of which are of the same function type (void()(Instructions):
using InstructionsUniquePtr = std::unique_ptr<Instructions, void(*)(Instructions*)>;
The delete function is:
void deleteInstructions(Instructions* instructions){ delete instructions;} void doNotDeleteInstructions(Instructions* instructions){}
One deletes the object, the other does nothing.
Use std::unique_The presence of PTR needs to be replaced by InstructionUniquePtr, the only pointer that can be constructed like this
if (method == BuildingMethod::fromSketch) return House(InstructionsUniquePtr(new Sketch, deleteInstructions)); if (method == BuildingMethod::fromBlueprint) return House(InstructionsUniquePtr(new Blueprint, deleteInstructions));
Unless the parameter comes from the stack, you can use the no-op deletor in this case:
Blueprint blueprint; House house(InstructionsUniquePtr(&blueprint, doNotDeleteInstructions));
As iaanus pointed out on Reddit, we should be aware that if unique_ptr moves out of the scope of the stack object and points to a resource that no longer exists.Use unique_afterPTR can cause memory corruption.
Also, as Bart mentioned in his comments, we should note that if the House constructor has multiple parameters, then we should declare unique_in a separate statementThe structure of ptr, like this:
InstructionsUniquePtr instructions(new Sketch, deleteInstructions); return House(move(instructions), getHouseNumber());
In fact, if an exception is thrown, a memory leak (cf Item 17 for valid c++) may occur.
Also, when we don't use custom deletors, we shouldn't use new directly, but std::make_unique, which allows you to pass parameters to construct object-oriented
Safety belt
If you are careful to avoid memory corruption, you can solve the initial problem with a custom deletor, but it will cause a slight change in the semantics of the passed parameters, which may be the source of many errors.
Typically, you hold an std::unique_ptr means being its owner.This means that you can modify the pointing object.However, if the object comes from the stack (or when it is passed through no-op deleter from elsewhere), the only pointer holds only references to externally owned objects.In this case, you don't want the only pointer to modify the object because it has side effects on the caller, which makes the code harder to infer.
Therefore, when using this technique, make sure the working pointer const object:
using InstructionsUniquePtr = std::unique_ptr< const Instructions, void(*)( const Instructions*)>; and the deleters become: void deleteInstructions( const Instructions* instructions){ delete instructions;} void doNotDeleteInstructions( const Instructions* instructions){}
This way, the only pointer won't cause trouble outside the class.This will save you a lot of debugging.
Custom deleters are ugly
When you see an expression defining a unique_ptr custom delete:
std::unique_ptr<const Computer, void(*)(const Computer*)>;
It is dense enough to be dangerous to your eyes if you look too long.We shouldn't spread this expression around the product code.So the most natural way is to write an alias
using ComputerConstPtr = std::unique_ptr<const Computer, void(*)(const Computer*)>;
Which performs better in the interface:
void plugIn(ComputerConstPtr computer);
But when we create unique_The ugly place still exists with the new instance of PTR because we have to pass one deletor at a time:
ComputerConstPtr myComputer(new Computer, deleteComputer);
Here we define deletion:
void deleteComputer(const Computer* computer){ delete computer;} void doNotDeleteComputer(const Computer* computer){}
This brings three problems.First, we should not specify anything if we want the smart pointer to delete its resources.This is what smart pointers were originally used for.
Of course, this is special because it may have to delete its resources because in some cases.But why is deleting its name burdened by special circumstances?
The second issue with namespaces is redundancy.Imagine us
Computer type s are in a nested namespace, as they often appear in production code:
namespace store { namespace electronics { namespace gaming { class Computer { // ... }; using ComputerConstPtr = std::unique_ptr<const Computer, void(*)(const Computer*)>; void deleteComputer(const Computer* computer); void doNotDeleteComputer(const Computer* computer); } } } store::electronics::gaming::ComputerConstPtr myComputer(new store::electronics::gaming::Computer, store::electronics::gaming::deleteComputer);
This is a very difficult line of code.And said very little.
Finally, we define a delete and a doNotDelete function for each type of delete we want to customize.Even if their implementation is not machine-specific or any other type.However, be aware that even if such a template is deleted:
template<typename T> void doDelete(const T* p) { delete p; } template<typename T> void doNotDeleteComputer(const T* x) { }
...It will not make the code simpler.In fact, when instantiating pointers, we still need to specify the template type:
store::electronics::gaming::ComputerConstPtr myComputer(new store::electronics::gaming::Computer, doDelete <store::electronics::gaming::Computer> );
A unique interface
Here's a suggestion from a fluent c++ reader, Sergio Adan, that addresses both of these issues: using the same interface for all custom deletors on all types.
This can be defined in another namespace (the technology namespace).In this example, we call this namespace util.
Then write Create Custom Unique_in this namespaceAll public code for ptr.
Let's call this helper MakeConstUnique as an instance.Here is its code:
namespace util { template<typename T> void doDelete(const T* p) { delete p; } template<typename T> void doNotDelete(const T* x) { } template<typename T> using CustomUniquePtr = std::unique_ptr<const T, void(*)(const T*)>; template<typename T> auto MakeConstUnique(T* pointer) { return CustomUniquePtr<T>(pointer, doDelete<T>); } template<typename T> auto MakeConstUniqueNoDelete(T* pointer) { return CustomUniquePtr<T>(pointer, doNotDelete<T>); } }
Use this code to use unique_for a specific type using a custom deletorWhen ptr, nothing else needs to be defined.For example, to create a unique_An instance of PTR that deletes its resources when it exceeds its scope, we write:
auto myComputer = util::MakeConstUnique(new store::electronics::gaming::Computer);
And create a resource that does not delete it:
auto myComputer = util::MakeConstUniqueNoDelete(new store::electronics::gaming::Computer);
The interesting thing about this interface is that:
In general, deletion is no longer mentioned.
Due to the return type MakeConstUnique, we can now use auto.
Note that all of this reduces us to one occurrence of the Computer namespace, not three.
Specific deleters
Now, if for some reason we don't want to call delete on a class machine, but instead call a specific specialized function, this might happen on a type from C, for example:
void deleteComputer(const Computer* computer) { specificFunctionThatFreesAComputer(computer); }
To continue using MakeConstUnique for this type, we can fully specialize this template function for this type of computer.By reopening the util namespace, we can do this on the module definition machine:
namespace util { template<> auto MakeConstUnique(store::electronics::gaming::Computer* pointer) { return CustomUniquePtr<store::electronics::gaming::Computer>(pointer, specificFunctionThatFreesAComputer); } }
In this case, the client code may not allocate its pointer with new.
Whichever way, a resource must be disposed of
: Now let's test our interface by adding some logs to the computer class:
class Computer { public: explicit Computer(std::string&& id) : id_(std::move(id)){} ~Computer(){std::cout << id_ << " destroyed\n";} private: std::string id_; };
Let's pass both the resources on the stack and the resources on the stack to our interfaces:
store::electronics::gaming::Computer c("stack-based computer"); auto myHeapBasedComputer = util::MakeConstUnique(new store::electronics::gaming::Computer("heap-based computer")); auto myStackBasedComputer = util::MakeConstUniqueNoDelete(&c);
Output:
Changes of deleter during the life of a unique_ptr
Let's use unique_with a custom deletorPtr, see in unique_When can the deletor be changed during the life cycle of the ptr?
Here's an example of a toy where we use unique_on int Ptr, with a customizable deletor:
using IntDeleter = void(*)(int*); using IntUniquePtr = std::unique_ptr<int, IntDeleter>;
One deletor is used for even numbers and the other for odd numbers:
void deleteEvenNumber(int* pi) { std::cout << "Delete even number " << *pi << '\n'; delete pi; } void deleteOddNumber(int* pi) { std::cout << "Delete odd number " << *pi << '\n'; delete pi; }
Assigning from another std::unique_ptr
The following code:
IntUniquePtr p1(new int(42), deleteEvenNumber); IntUniquePtr p2(new int(43), deleteOddNumber); p1 = move(p2);
p1 contains an even number with appropriate deletors and is taking over ownership of resources in p2.The question is: How will it destroy this resource? Will it use the deletor it was built with, or will it bring with it the deletor of P2 along with its ownership of the resource?
Here's what this program outputs (delete the program prints out the information - see the code at the top of the article):
Delete each resource with the correct deletors, which means that assignments do bring deletors.This makes sense because otherwise the resource will not be released using the correct deletor.
Resetting the pointer
Change std::unique_Another way to call the reset method of a resource contained in a PTR is as follows:
std::unique_ptr<int> p1(new int(42)); p1.reset(new int(43));
The reset method calls the deletor for the current resource (42) and then processes the new resource
(43).
However, the reset method accepts only one parameter, the new resource.It cannot be passed with this new resource to a deletor.Therefore, in our example, it can no longer be used directly for even and odd numbers.Indeed, the following rules:
IntUniquePtr p1(new int(42), deleteEvenNumber); p1.reset(new int(43)); // can't pass deleteOddNumber
This is incorrect for us.
In fact, by using unique_Get_of PTRThe deleter method returns the deleter through a non-const reference, and we can manually change the deleter in a separate statement (thanks to Marco Arena for pointing out this):
p1.get_deleter() = deleteOddNumber;
But why doesn't the reset have a delete parameter? And how to transfer the new resource to std::unique_in a statementPTR and its appropriate deletors?
Howard Hinnant, he is std::unique_One of the primary designers and authors of the PTR component answered this question about stack overflow:
Here's how to use his answer in our initial example:
IntUniquePtr p1(new int(42), deleteEvenNumber); p1 = IntUniquePtr(new int(43), deleteOddNumber);
How to Return a Smart Pointer AND Use Covariance
The problem: Covariant return type vs. smart pointers
c++ supports covariant return types.That is, you can have the following code:
struct Base {}; struct Derived : Base {}; struct Parent { virtual Base * foo(); } ; struct Child : Parent { virtual Derived * foo() override ; } ;
Here, we want the foo method from Child to return Base *, so that we can successfully override (and compile!) using the covariant return type, and we can actually replace Base with any of its derived types.For example, derived.
This applies to pointers, and references...But when you try to use the smart pointer:
#include <memory> struct Base {}; struct Derived : Base {}; struct Parent { virtual std::unique_ptr<Base> foo(); } ; struct Child : Parent { virtual std::unique_ptr<Derived> foo() override ; } ;
Compiler error
use cases
Since this is a common problem, let's use a broad panel of increasingly complex use cases:
By dealing with all these situations in a natural way, the solution should be available to most production problems.
Preamble: Separation of concerns + private virtual function
We'll split it into two member functions instead of a cloned member function for everything.In the following code:
class some_class { public: std::unique_ptr<some_class> clone() const { return std::unique_ptr<some_class>(this->clone_impl()); } private: virtual some_class * clone_impl() const { return new some_class(*this) ; } };
The first function clone_impl() uses the copy constructor to perform the actual work of cloning.
It provides a powerful guarantee (as long as the copy constructor provides it) and transfers ownership of the pointer to the newly created object.Although this is usually unsafe, we assume that in this case, no one can call this function except the clone() function, which is done by clone_Private access to impl() to enforce.
The second function, clone(), retrieves the pointer and gives ownership to unique_ptr.
This function itself will not fail, so it provides a connection to clone_impl() is the same strong guarantee.
Simple Hierarchy: Covariance + Name hiding
Using the above techniques, we can now produce a simple OO hierarchy:
class cloneable { public: virtual ~cloneable() {} std::unique_ptr<cloneable> clone() const { return std::unique_ptr<cloneable>(this->clone_impl()); } private: virtual cloneable * clone_impl() const = 0; }; ///////////////////////////////////////////////////////////////////// class concrete : public cloneable { public: std::unique_ptr<concrete> clone() const { return std::unique_ptr<concrete>(this->clone_impl()); } private: virtual concrete * clone_impl() const override { return new concrete(*this); } }; int main() { std::unique_ptr<concrete> c = std::make_unique<concrete>(); std::unique_ptr<concrete> cc = c->clone(); cloneable * p = c.get(); std::unique_ptr<clonable> pp = p->clone(); }
See what we've done?
By separating concerns, we can use covariance at each level of the hierarchy to generate clone_impl member function, returns the type of pointer we want.
Using an annoying feature in c++, name hiding (for example, when a name is declared in a derived class, that name hides all symbols of the same name in the base class), we hide it
The member function clone() returns the smart pointer of the exact type we want.
When cloning from a concrete, we get a unique_ptr, when cloning from a cloneable, we get a unique_ptr.
Clone_for transfer of ownership using unsafe raii Imp member functions, which you may find uncomfortable, are alleviated because they are private and can only be called by cloning.This limits the risk because users of the class cannot call it incorrectly.
This solves the problem, but adds some template code.
Simple Hierarchy, v2: Enter the CRTP
CRTP is a c++ style that supports injecting derived class names into template bases.You can learn about it in the series on CRTP on Fluent c++.
We will use it to declare a method in the CRTP base class with the correct derived prototype, then inject the method into the derived class itself through inheritance:
template <typename Derived, typename Base> class clone_inherit<Derived, Base> : public Base { public: std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const override { return new Derived(*this); } };
clone_inherit is a CRTP that knows its derived classes and all its direct base classes.It covariants clone_as usualImpl() and hide clone() member functions, but they use a cast to traverse the type hierarchy.
This allows us to change the specific classes defined above to:
class concrete : public clone_inherit<concrete, cloneable> { }; int main() { std::unique_ptr<concrete> c = std::make_unique<concrete>(); std::unique_ptr<concrete> cc = b->clone(); cloneable * p = c.get(); std::unique_ptr<clonable> pp = p->clone(); }
As you can see, concrete classes are no longer messy.
This effectively adds polymorphic covariant clones () to the class hierarchy.
This CRTP is the basis of our common solution: the next step will be to build on it.
Multiple Inheritance: Variadic templates to the rescue
One complication of the OO hierarchy is multiple inheritance.
In our example, how do we extend our solution to support cases where a specific class inherits from two base classes that provide the same cloning characteristics?
The solution first requires two base classes, foo and bar, to provide clone / clone_impl member function:
class foo { public: virtual ~foo() = default; std::unique_ptr<foo> clone() const { return std::unique_ptr<foo>(this->clone_impl()); } private: virtual foo * clone_impl() const = 0; }; ///////////////////////////////////////////////////////////////////// class bar { public: virtual ~bar() = default; std::unique_ptr<bar> clone() const { return std::unique_ptr<bar>(this->clone_impl()); } private: virtual bar * clone_impl() const = 0; };
Here are some sample files, but we'll work on them later.Now we have to solve the inheritance problem, and c++ 11 gives us a simple solution: variable parameter templates.
We just need to modify clone_inherit CRTP to support it:
template <typename Derived, typename ... Bases> class clone_inherit : public Bases... { public: std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const override { return new Derived(static_cast<const Derived & >(*this)); } };
Now we can use it to write specific classes:
class concrete : public clone_inherit<concrete, foo, bar> { };
Last but not least, we can use classes with covariance and smart pointers:
int main() { std::unique_ptr<concrete> c = std::make_unique<concrete>(); std::unique_ptr<concrete> cc = c->clone(); foo * f = c.get(); std::unique_ptr<foo> ff = f->clone(); bar * b = c.get(); std::unique_ptr<bar> bb = b->clone(); }
Multiple Inheritance v2: Specialization to the rescue
Now let's solve this problem: both foo and bar offer the same "clonable" feature.In our case, both should be destroyable.
The solution is to specialize clone_inherit handles cases where no base class is required, provides a virtual destructor, and inherits foo and bar from it:
template <typename Derived, typename ... Bases> class clone_inherit : public Bases... { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const override { return new Derived(static_cast<const Derived & >(*this)); } }; ///////////////////////////////////////////////////////////////////// template <typename Derived> class clone_inherit<Derived> { public: virtual ~clone_inherit() = default; { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const = 0; }; This way, we can now write: class foo : public clone_inherit<foo> { }; ///////////////////////////////////////////////////////////////////// class bar : public clone_inherit<bar> { }; ///////////////////////////////////////////////////////////////////// class concrete : public clone_inherit<concrete, foo, bar> { };
Last but not least, we can use classes with covariance and smart pointers:
int main() { std::unique_ptr<concrete> c = std::make_unique<concrete>(); std::unique_ptr<concrete> cc = c->clone(); foo * f = c.get(); std::unique_ptr<foo> ff = f->clone(); bar * b = c.get(); std::unique_ptr<bar> bb = b->clone(); }
Deep Hierarchy: Abstracting
Another complication of OO hierarchies is that they can go into two levels:
The problem is that, as Scott Meyers suggested, nonleaf classes should not instantiate themselves (more efficient c++, item 33).
In our example, clone_in a non-leaf classThe impl method must be pure virtual.
Therefore, our solution must support the declaration clone_impl pure virtual or implementation choice.
First, we add a specialized type for "marking" a type:
template <typename T> class abstract_method { };
Then we partially specialized clone_againInherit class to use this type, which means
(due to previous specialization), four different clones_Inherit implementation:
// general: inheritance + clone_impl implemented template <typename Derived, typename ... Bases> class clone_inherit : public Bases... { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const override { return new Derived(static_cast<const Derived & >(*this)); } }; ///////////////////////////////////////////////////////////////////// // specialization: inheritance + clone_impl NOT implemented template <typename Derived, typename ... Bases> class clone_inherit<abstract_method<Derived>, Bases...> : public Bases... { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const = 0; }; ///////////////////////////////////////////////////////////////////// // specialization: NO inheritance + clone_impl implemented template <typename Derived> class clone_inherit<Derived> { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const override { return new Derived(static_cast<const Derived & >(*this)); } }; ///////////////////////////////////////////////////////////////////// // specialization: NO inheritance + clone_impl NOT implemented template <typename Derived> class clone_inherit<abstract_method<Derived>> { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const = 0; };
class cloneable : public clone_inherit<abstract_method<cloneable>> { }; ///////////////////////////////////////////////////////////////////// class abstracted : public clone_inherit<abstract_method<abstracted>, cloneable> { }; ///////////////////////////////////////////////////////////////////// class concrete : public clone_inherit<concrete, abstracted> { }; int main() { std::unique_ptr<concrete> c = std::make_unique<concrete>(); std::unique_ptr<concrete> cc = c->clone(); abstracted * a = c.get(); std::unique_ptr<abstracted> aa = a->clone(); cloneable * p = c.get(); std::unique_ptr<clonable> pp = p->clone(); }
Diamond Inheritance: Virtual-ing
Another complication of the OO hierarchy is that we can have a diamond inheritance:
In c++, this means we can choose whether the base class is virtual inherited or not?
Therefore, this choice must be made by clone_inherit provided.The problem is that declaring virtual inheritance is much more complex because of the template parameter package...Or?
Let's write a class to indirectly:
template <typename T> class virtual_inherit_from : virtual public T { using T::T; };
This class actually applies virtual inheritance to its base class T, which is exactly what we want.What we need now is to use this class to explicitly show our virtual inheritance needs:
class foo : public clone_inherit<abstract_method<foo>, virtual_inherit_from<cloneable>> { }; class bar : public clone_inherit<abstract_method<bar>, virtual_inherit_from<cloneable>> { }; ///////////////////////////////////////////////////////////////////// class concrete : public clone_inherit<concrete, foo, bar> { }; int main() { std::unique_ptr<concrete> c = std::make_unique<concrete>(); std::unique_ptr<concrete> cc = c->clone(); foo * f = c.get(); std::unique_ptr<foo> ff = c->clone(); bar * b = c.get(); std::unique_ptr<bar> bb = c->clone(); cloneable * p = c.get(); std::unique_ptr<cloneable> pp = p->clone(); }
The whole package
The whole clone-ing code is: ///////////////////////////////////////////////////////////////////// template <typename T> class abstract_method { }; ///////////////////////////////////////////////////////////////////// template <typename T> class virtual_inherit_from : virtual public T { using T::T; }; ///////////////////////////////////////////////////////////////////// template <typename Derived, typename ... Bases> class clone_inherit : public Bases... { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } 54 protected: // desirable, but impossible in C++17 // see: http://cplusplus.github.io/EWG/ewg-active.html#102 // using typename... Bases::Bases; private: virtual clone_inherit * clone_impl() const override { return new Derived(static_cast<const Derived & >(*this)); } }; ///////////////////////////////////////////////////////////////////// template <typename Derived, typename ... Bases> class clone_inherit<abstract_method<Derived>, Bases...> : public Bases... { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived *>(this->clone_impl())); } protected: // desirable, but impossible in C++17 // see: http://cplusplus.github.io/EWG/ewg-active.html#102 // using typename... Bases::Bases; private: virtual clone_inherit * clone_impl() const = 0; }; ///////////////////////////////////////////////////////////////////// template <typename Derived> class clone_inherit<Derived> { public: virtual ~clone_inherit() = default; 55 std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const override { return new Derived(static_cast<const Derived & >(*this)); } }; ///////////////////////////////////////////////////////////////////// template <typename Derived> class clone_inherit<abstract_method<Derived>> { public: virtual ~clone_inherit() = default; std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl())); } private: virtual clone_inherit * clone_impl() const = 0; }; ///////////////////////////////////////////////////////////////////// ... and the user code is: ///////////////////////////////////////////////////////////////////// class cloneable : public clone_inherit<abstract_method<cloneable>> { }; 56 ///////////////////////////////////////////////////////////////////// class foo : public clone_inherit<abstract_method<foo>, virtual_inherit_from<cloneable>> { }; ///////////////////////////////////////////////////////////////////// class bar : public clone_inherit<abstract_method<bar>, virtual_inherit_from<cloneable>> { }; ///////////////////////////////////////////////////////////////////// class concrete : public clone_inherit<concrete, foo, bar> { };
Overall, this is not bad.
Will we use it in production code? While this set of technologies is interesting, it won't compile on Visual Studio 2017 (virtual inheritance, diamond, and covariance don't mix well)
Visual Studio), in our case, is a showstopper.However, it can be compiled with at least GCC 5.4.0 + and Clang 3.8.0 +.
This set of techniques shows how by using a clever and simple combination of two orthogonal c++ paradigms (object-oriented and generic (templates), we can break down code to produce concise results that are difficult or impossible in other methods
c language.
It also shows a series of techniques that can be applied elsewhere (simulating covariance, providing indirect inheritance of features), each of which relies on c++ features assembled like lego fragments to produce the desired results.
I think it's cool.