Continuing the post on the factory method addresses two issues: slicing and ownership semantics.
Patterns are an important abstraction in modern software development. They offer well-defined terminology, clean documentation, and learning from the best. The classic book “Design Patterns: Elements of Reusable Object-Oriented Software” (Design Patterns for short) contains 23 patterns, including the factory method, which is one of the creation patterns.
In the last episode of this blog, I introduced the factory method: “Software Development: The Design Pattern Factory Method for Creating Objects”. My implementation had two major problems: slicing and ownership semantics. Today I will fix these problems.
As a reminder, here’s a simplified and slightly modified implementation of the factory method from my last article. First, this implementation only supports the member function clone
; second should include the base class Window
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';
}
slicing
First of all: what is slicing?
- slicing means that you want to copy an object during allocation or initialization and only get a part of the object.
Slicing is one of the darkest corners of C++, as this simple example illustrates:
// 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)
}
Expressions (1), (2) and (3) all have the same effect: The derived part of d
will be removed. But that’s probably not intentional.
Why is slicing a problem for the example factoryMethodWindowIssues.cpp?
Let me quote the C++ Core Guidelines: C.67: A polymorphic class should suppress public copy/move. This brings up the next question: what is a polymorphic class?
- One polymorphic class is a class that defines or inherits one or more virtual functions.
Here’s the problem: Window
in program factoryMethodWindowIssues.cpp
is a polymorphic class that does not suppress public copy/move. A Window
can become a victim of slicing, as the following code snippet illustrates. I only have the reference from the function signature of the function cloneWindow
away:
// Concrete Creator or Client
Window* cloneWindow(Window oldWindow) {
return oldWindow.clone();
}
Because the factory function cloneWindow
takes its argument as a copy and no longer as a reference, slicing comes into play.
Here is the corrected 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 have the copy constructor (1) and copy assignment operator (2) on delete
set. Because of the declared copy constructor, the class supports Window
no move semantics. Also, the compiler does not create the default constructor. That’s why I have to request it (1).
The Microsoft Visual C++ compiler error message sums it up:
ownership semantics
In general, one knows the implementation of the factory function cloneWindow
Not. cloneWindow
returns a pointer Window
return. Pointers have an implicit flaw. They model two completely different semantics: ownership and lending.
- possession: The caller is for that
Window
responsible and must destroy it. The behavior models the programfactoryMethodWindowsIssues.cpp
.
- loan: The callee is responsible for the window and lends it to the caller.
Let me emphasize this again:
- Owner: You are the owner of the window, you have to take care of it and destroy it. Otherwise there will be a memory leak.
- borrower: You are not the owner of the window and must not destroy it. Otherwise it will be deleted twice.
How can we overcome this design flaw? The rescue consists of two components. A weak one based on discipline and a strong one based on the type system.
The weak rescue
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. If you want to transfer ownership, use a smart pointer. There are two ways to do this:
std::unique_ptr<Widget>
: The return of astd::unique_ptr<Widget>
means that the caller is the owner. Thatstd::unique_ptr<Widget>
behaves like a local variable. If it goes out of scope, it is automatically destroyed.
std::shared_ptr<Widget>
: Returning astd::shared_ptr<Widget>
means that the caller and the callee share ownership. If neither the caller nor the calleestd::shared_ptr<Widget>
more needed, it will be destroyed automatically.
The following program factoryMethodUniquePtr.cpp
uses one std::unique_ptr<Window>
to explicitly transfer ownership. In addition, a std::unique_ptr cannot be copied. Consequently, the factory function creates a new one std::unique_ptr<Widget>
.
// 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';
}
Every create
-Member function enters std::unique_ptr<Widget>
back, but generates one under the hood std::unique_ptr<Widget>
a std::unique_ptr<DefaultWindow>
or a std::unique_ptr<FancyWindow>.
Therefore, the polymorphic behavior remains the createWindow
-Get function.
In addition, this dissolves std::unique_ptr
based implementation solves the slicing problem automatically since a std::unique_ptr<Widget>
cannot be copied.
What’s next?
The final program factoryMethodUniquePtr.cp
p is correct. It overcomes the problems of slicing and ownership semantics. In my next article, I’ll take a closer look at the most controversial pattern in the Design Patterns book: Singleton.