C++ Design Patterns – Dependency on New or Shared_ptr

cdesigndesign-patternsobject-oriented

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 is ConcreteClassA or ConcreteClassB. 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.

  • Raw pointers imply no ownership and should therefore be avoided. Should I delete this or is the object just borrowed? Unclear. Raw pointers are a recipe for memory leaks and segfaults. It is difficult to write exception-safe code with raw pointers.
  • References mean that the object is temporarily borrowed, and is owned by some other object. Convenient: a reference can never be a null pointer, and does not have to be dereferenced explicitly.
  • A 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.
  • A 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.