I'm trying to learn the best practices for code design and reuse in C++ so I am going through the well known GoF Design Patterns Elements of Reusable Object-Oriented Software.
I noticed that nearly all design patterns use dynamically allocated objects. To prevent memory leaks, I'm leaning towards using shared_ptr
for all of these classes. Basically, the shared_ptr
usually points to an abstract class interface and that is how objects interact with each other.
When I look up discussion on shared_ptrs for C++, people say that if you are using shared_ptrs everywhere in your C++ code, you are probably doing things wrong. Do these people know what they are talking about or should I adamantly stick to using dynamic heap allocation for all my objects?
Here is one example I found of commenters saying to avoid this design practice:
https://stackoverflow.com/questions/25357499/is-it-possible-in-any-way-using-c-preprocessor-etc-to-replace-shared-ptrt
Example of common interface
std::shared_ptr<Inferface> item1(new ConcreteClassA());
std::shared_ptr<Inferface> item2(new ConcreteClassB());
item1->Action();
item2->Action();
Example of avoiding common interfaces and heap allocation
void Action(ConcreteClassA item){
item.Action();
}
void Action(ConcreteClassB item){
item.Action();
}
ConcreteClassA item1;
ConcreteClassB item2;
Action(item1);
Action(item2);
The first example is clearly superior for code reuse since I don't have to write new functions for each new class, but this involves shared_ptrs and heap allocation. This is a very simplified example of why design patterns are useful and obviously it get's much more complicated than this.
I'm at a point of confusion on what approach is considered common practice for software design with C++. Let's say I'm making application software that I will need to maintain over multiple years, like a WYSIWYG text editor example from the GoF book. When people say, you shouldn't extensively use shared_ptr to manage objects do they mean I should use other smart pointers/raw pointers or is the common C++ practice to avoid dynamic heap allocation altogether?
To me, OOP make more sense for a language like Java, where garbage collection is automatic and interfaces are a part of the language.
Is the industry practice to follow design patterns using dyanmic allocation when developing applications in C++? If so, are smart pointers the right way to go?
Best Answer
The GoF design patterns are about clever ways to use polymorphism to keep a design extensible. For example, the strategy pattern lets us supply different strategy implementations without having to recompile any code that uses the strategy.
Object oriented techniques (polymorphism/dynamic dispatch) mean that when we have an object, we don't have to know it's actual type. It should be sufficient to know its
Interface
, not whether it isConcreteClassA
orConcreteClassB
. You are not using dynamic dispatch when the type of an object is fully known, e.g. when the object is stored by value in a local variable. When you use an overloaded function, that is resolved at compile time via static dispatch and does not have the extensibility properties of dynamic dispatch. For more details, please read my article Dynamic vs. static dispatch.When we only know an object's interface and not its dynamic type, that requires we hold the object via some pointer. This could be a raw pointer, a reference, or a smart pointer. These different pointer types imply different ownership semantics.
std::unique_ptr
transfers ownership of the pointed-to object. Once the smart pointer is destroyed automatically, the pointed-to object will also be deleted (→ RAII). Unless a reference or a shared pointer is more appropriate, this should be your default pointer type.std::shared_ptr
indicates shared ownership. The pointed-to object is automatically reference-counted. Once the last shared pointer to reference that object is destroyed, the pointed-to object is also deleted. The reference counting implies some runtime overhead.Therefore, the advice to avoid shared pointers is correct. Many object graphs have clear ownership and do not need a garbage collection approach like reference counting. Yet if you need it, the option is still there. Note that shared pointers are not quite like Java-style garbage collection, because you must still avoid reference cycles (e.g. by using weak pointers).
The GoF patterns only consider object-oriented techniques, yet C++ is much more than that. Notably, templates can sometimes address similar problems. But templates and OOP are fundamentally different. Importantly, changing a template requires you to recompile all dependent code, whereas OOP techniques can isolate components.