The Factory Method (Slicing and Ownership Semantics)
In the last installment of this blog, I introduced the Factory Method: Creational Patterns: Factory Method 1. My implementation had two serious issues: slicing and ownership semantics. Today, I fix these issues.
To remind you, here is a simplified and slightly modified implementation of the Factory Method in my last post. First, this implementation support only the member function clone
; second, also the base class Window
should be cloneable.
// factoryMethodWindowIssues.cpp #include <iostream> // Product class Window{ public: virtual Window* clone() { std::cout << "Clone Window" << '\n'; return new Window(*this); } virtual ~Window() {}; }; // Concrete Products class DefaultWindow: public Window { DefaultWindow* clone() override { std::cout << "Clone DefaultWindow" << '\n'; return new DefaultWindow(*this); } }; class FancyWindow: public Window { FancyWindow* clone() override { std::cout << "Clone FancyWindow" << '\n'; return new FancyWindow(*this); } }; // Concrete Creator or Client Window* cloneWindow(Window& oldWindow) { return oldWindow.clone(); } int main() { std::cout << '\n'; Window window; DefaultWindow defaultWindow; FancyWindow fancyWindow; const Window* window1 = cloneWindow(window); const Window* defaultWindow1 = cloneWindow(defaultWindow); const Window* fancyWindow1 = cloneWindow(fancyWindow); delete window1; delete defaultWindow1; delete fancyWindow1; std::cout << '\n'; }
The program produces the expected polymorphic behavior.
Slicing
First: What is slicing?
- Slicing means you want to copy an object during assignment or initialization, and you get only a part of the object.
Slicing is one of the darkest corners of C++. Let me give you a simple example:
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
// slice.cpp #include <iostream> #include <typeinfo> struct Base { }; struct Derived : Base { }; void displayTypeinfo(const Base& b) { std::cout << typeid(b).name() << '\n'; } void needB(Base b) { displayTypeinfo(b); }; int main() { Derived d; Base b = d; displayTypeinfo(b); // (1) Base b2(d); displayTypeinfo(b2); // (2) needB(d); // (3) }
The expressions (1), (2), and (3) have all the same effect: The Derived
part of d
is removed. I assume that was not your intention.
You may ask yourself. Why is slicing an issue for the example factoryMethodWindowIssues.cpp. Let me quote the C++ Core Guidelines: C.67: A polymorphic class should suppress public copy/move.
Okay, there is a new question: What is a polymorphic class?
- A polymorphic class is a class that defines or inherits at least one virtual function.
Here is the issue. Window
in the program factoryMethodWindowIssues.cpp is a polymorphic class, and it does not suppress public copy/move. A Window
can be a victim of slicing. The following program exemplifies this. I just removed the reference from the function signature of the function cloneWindow
:
// Concrete Creator or Client Window* cloneWindow(Window oldWindow) { return oldWindow.clone(); }
Due to the fact that the factory function cloneWindow
takes its argument by copy and not by reference anymore, slicing kicks in.
Let’s see what happens when I follow rule C.67: A polymorphic class should suppress public copy/move.
Here is the fixed program:
// factoryMethodWindowSlicingFixed.cpp #include <iostream> // Product class Window{ public: Window() = default; // (3) Window(const Window&) = delete; // (1) Window& operator = (const Window&) = delete; // (2) virtual Window* clone() { std::cout << "Clone Window" << '\n'; return new Window(*this); } virtual ~Window() {}; }; // Concrete Products class DefaultWindow: public Window { DefaultWindow* clone() override { std::cout << "Clone DefaultWindow" << '\n'; return new DefaultWindow(*this); } }; class FancyWindow: public Window { FancyWindow* clone() override { std::cout << "Clone FancyWindow" << '\n'; return new FancyWindow(*this); } }; // Concrete Creator or Client Window* cloneWindow(Window oldWindow) { // (4) return oldWindow.clone(); } int main() { std::cout << '\n'; Window window; DefaultWindow defaultWindow; FancyWindow fancyWindow; const Window* window1 = cloneWindow(window); const Window* defaultWindow1 = cloneWindow(defaultWindow); const Window* fancyWindow1 = cloneWindow(fancyWindow); delete window1; delete defaultWindow1; delete fancyWindow1; std::cout << '\n'; }
I deleted the copy constructor (line 1) and copy assignment operator (line 2). Due to the declared copy constructor, the class Window
supports no move semantics. Additionally, the default constructor is also not declared. Therefore, I have to default it (line 1).
The error message of the Microsoft Visual C++ compiler comes directly to the point:
The necessary copy constructor is deleted.
Okay. Let me write about the ownership issue of the program.
Ownership Semantics
In general, you don’t know the implementation of the factory function cloneWindow
. cloneWindow
returns a pointer to Window
. Pointers have, by design, a flaw. They model two completely different semantics: ownership and borrowing.
- Ownership: the caller is responsible for the Windows and must destroy it; this is the behavior that program
factoryMethodWindowsIssues.cpp
modeled - Borrowing: the callee is responsible for the Window and borrows it from the caller
Let me emphasize this one more.
- Owner: You are the owner of the
Window
. You have to take care of it and destroy it. If not, you have a memory leak. - Borrower: You are not the owner of the Window. You can not destroy it. If you destroy it, you have a double delete.
How can we overcome this design flaw? The rescue has two parts. A weak one based on discipline and a strong one based on the type system.
The Weak Rescue
The weak rescue is based on discipline: In modern C++, we don’t transfer ownership with a raw pointer.
The Strong Rescue
The strong rescue is based on the type system. When you want to transfer ownership, use a smart pointer. You have two choices:
std::unique_ptr<Window>
: Returning astd::unique_ptr<Window>
means that the caller is the owner. Thestd::unique_ptr<Window>
behaves such as a local. When it goes out of scope, it is automatically destroyed.std::shared_ptr<Window>
: Returning astd::shared_ptr<Window>
means that the caller and the called share ownership. When neither the caller nor the callee needs thestd::shared_ptr<Window>
anymore, it is automatically destroyed.
The following program factoryMethodUniquePtr.cpp
uses a std::unique_ptr<Window>
to explicitly transfer ownership. Additionally, a std::unique_ptr
can not be copied. Consequentially, the factory function creates a new std::unique_ptr<Window>
.
// factoryMethodUniquePtr.cpp
#include <iostream> #include <memory> // Product class Window{ public: virtual std::unique_ptr<Window> create() { std::cout << "Create Window" << '\n'; return std::make_unique<Window>(); } }; // Concrete Products class DefaultWindow: public Window { std::unique_ptr<Window> create() override { std::cout << "Create DefaultWindow" << '\n'; return std::make_unique<DefaultWindow>(); } }; class FancyWindow: public Window { std::unique_ptr<Window> create() override { std::cout << "Create FancyWindow" << '\n'; return std::make_unique<FancyWindow>(); } }; // Concrete Creator or Client auto createWindow(std::unique_ptr<Window>& oldWindow) { return oldWindow->create(); } int main() { std::cout << '\n'; std::unique_ptr<Window> window = std::make_unique<Window>(); std::unique_ptr<Window> defaultWindow = std::make_unique<DefaultWindow>(); std::unique_ptr<Window> fancyWindow = std::make_unique<FancyWindow>(); const auto window1 = createWindow(window); const auto defaultWindow1 = createWindow(defaultWindow); const auto fancyWindow1 = createWindow(fancyWindow); std::cout << '\n'; }
Finally, here is the output of the program.
Each create
member function returns a std::unique_ptr<Window>
, but creates under the hood a std::unique_ptr<Window>
, a std::unique_ptr<DefaultWindow>
, or a std::unique_ptr<FancyWindow>
. Therefore, the polymorphic behavior of the createWindow
function is preserved.
Additionally, this implementation based on std::unique_ptr
solves the slicing issue because a std::unique_ptr<Window>
cannot be copied.
What’s Next?
The final program factoryMethodUniquePtr.cpp
is fine. It overcomes the slicing and ownership issues. Let me elaborate in my next post on the most controversial pattern of the Design Patterns book: the Singleton pattern.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!