3

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?

2 Answers 2

8

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.

5
  • 3
    Ok so what I get from this is that blind use of shared_ptr is not good practice, but in general smart pointers are the way to go for designing good OOP C++ code. I should make sure I understand the ownership of objects and use unique_ptr rather than defaulting to shared_ptr. Commented Nov 8, 2018 at 23:38
  • 1
    It's generally OK to receive an optional polymorphic value through a raw pointer. Although you may want to mark it as a Observer<T> (where Observer is just a template alias for T *)
    – Caleth
    Commented Nov 9, 2018 at 10:18
  • When you say "Many object graphs have clear ownership and do not need a garbage collection approach like reference counting" I feel offended because we don't use "garbage collection" in C++ Commented Feb 7, 2019 at 9:33
  • 1
    @RaulLapeiraHerrero As discussed in this answer, std::shared_ptr allows us to opt in to reference counting for individual objects. There are also libraries like the Boehm GC, which provides a mark-and-sweep garbage collector for C and C++. That may still be useful, even though C++'s RAII can cover a huge amount of memory management that would require GC in other languages.
    – amon
    Commented Feb 7, 2019 at 10:23
  • @amon to be honest I was mostly trying a pun at ISO C++ expert's typical comments Commented Feb 7, 2019 at 11:15
4

A better 'rule' is 'No naked new'; one should use the appropriate smart pointer where applicable:

std::unique_ptr

used for unique allocations that will not be shared except by weak pointers. A unique pointer has a single owner

std::shared_ptr

used for shared allocations that do not have an owner

std::weak_ptr

used for non ownership references


Many uses of design patterns can be implemented with unique_ptr returning weak_ptr references. Only use a shared pointer for allocations that don't have an owner, or will outlive their creator.

1
  • Is it possible to pass a pointer of a unique_ptr in a weak_ptr? I don't think they are compatible. unique_ptr and its content are not meant to be passed someone else in any way but by move, right? Do I miss something here? Commented Jun 30, 2022 at 19:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.