Document number: P1846R0
Date: 2019-08-05
Reply-to: John McFarlane, [email protected]
Audience: SG20
Classes are at the heart of type safety in C++ and their utility will long outlive the OOP era. But there's a huge difference between designing a new class and utilising an existing class. As experts, we take for granted just how much knowledge is required to safely write a class. And with every revision of the standard, the need for users to write their own classes decreases. I propose that teaching of class design should be treated as an advanced skill which comes after a long period of using and appreciating the classes provided by the standard library.
P1389R0 entitled "Standing Document for SG20: Guidelines for Teaching C++ to Beginners", lists beginner topics in four stages. The first stage, "fundamentals", lists 24 topics including "designing and using classes". This suggests that designing classes should be taught early in a student's curriculum.
While I agree that C++ is next to useless without the ability to use classes, I argue that class design is increasingly difficult and unnecessary and should be left until far later.
I identify three influences which, above all others, have led me to this conclusion.
Firstly, advocates of modern and functional programming often discourage the use of class hierarchies as the route to API design. For example, "Inheritance Is The Base Class of Evil", leaves little room for interpretation.
Secondly, when preparing introductory material for presentation to engineers who are new to C++, it is easy to imagine a 60-minute talk on classes which could make them 'dangerous'. It is much harder to imagine a 180-minute talk that would make them 'safe'. Topics such as good API design, resource management, object lifetime and special functions all require careful explanation. The number of keywords alone which are devoted to writing a class is dizzying.
Thirdly, on several occasions, I have been asked an architectural question regarding a design which is heavily object oriented. In each case, all the classes could be replaced with functional programming paradigms and this reduced the cognitive load necessary to resolve the question.
If knowledge of class design is to be deferred, this leaves a gap which must be filled. Most of the remainder of the paper explains how this might be done.
I find it ironic that a programming construct so closely related to the S.O.L.I.D. principles should itself embody so many concerns. This section explores some of the functionality a class can provide and why it is not necessary to duplicate that functionality in a new class.
Dynamic polymorphism is the headline feature which classes provide. (It is a given that static polymorphism is often a better choice.) Assuming a clean design involving a clear division between interface and implementation, a great deal of boilerplate code is necessary. As an understanding of interface purity develops, a base class will refine down to two virtual functions: a 'doing' function and a destructor.
At that point, what we find ourself with is
a verbose reimplementation of function
without the value semantics.
On rare occasions,
a statically allocated object of an implementation class can outperform
a function
object.
Conversely, SBO means that the opposite can also be true.
Classes can be used to group objects together. But increasingly,
there are simpler ways to achieve this.
A struct
is the simplest example.
Where the components require no names, tuple
is ideal.
RAII continues to distinguish C++ from almost every other language.
But its power is now largely hidden from plain sight behind value types.
The vast majority of instances where an object manages a resource,
that resource is free store memory and for this, we have unique_ptr
.
Most of what is left is covered by unique_resource
.
A common abuse of classes is as containers for definitions. This is probably an interloper from OO languages such as Java. Advice such as rule 44 of C++ Coding Standards: "Prefer writing nonmember nonfriend functions" is a great coverall which discourages this bad practices — among others. Regardless, we have namespaces for grouping definitions together.
In situations where one might think to use dynamic_cast
the first reaction is: you probably don't want to do that.
But there are situations where alternatives are not representable
via an interface.
In such cases, any
or variant
might be what you're looking for.
There are situations where a user would like a carbon copy of an existing class but with distinct semantics. For example:
using sanitized_string = string;
sanitized_string sanitize(string unsanitized);
sanitized_string good{sanitize("<script>alert('pwned!');</script>")};
sanitized_string bad{"<script>alert('pwned!');</script>"};
Here the compiler doesn't stop the user
from instantiating insecure object bad
.
If sanitized_string
instead inherits from string
class sanitized_string : string {};
a degree of type safety is introduced. Whether or not this is a sensible use of inheritance, it is one reason why users might consider defining a class.
The traditional way to create a function object is to define a class with an overloaded function call operator. Lambda expressions largely replace this need with a more terse syntax.
Restricting access to variables can be achieved in various ways.
The private
keyword is just one of these.
Function-level encapsulation is a very powerful alternative
which is often overlooked because of its ubiquity.
Lambda capture follows naturally from function-level encapsulation
and often do precisely the same job as class-based encapsulation.
Enforcing an invariant through use of access specifiers
remains a compelling use case for a class.
I'm talking here about the case where one or more variables
must be in a subset of their possible states.
Where the state isn't expected to change,
the same effect can be achieved through the const
type qualifier.
Understanding const
should be prioritized over class
and private
.
One thing to be observed about the above laundry list of features is that many of them are implemented using classes. There is no contention that classes remain a powerful and necessary abstraction. One might even characterise functional programming as OOP's logical conclusion.
At some point, the student will find a gap in the set of language and library features. It is the job of WG21 to plug these gaps in descending order of occurrence. Until then, they have reason to define a class of their own.
Having learned all of the above programming techniques — some of which depend on library classes — the student should be in a good position to appreciate when is a good time to take the plunge and author a class.
I have concluded that invariants are the remaining 'killer app' for the user-land class definition. Invariants make an excellent way to frame the topic of good API design — one of the hardest skills a C++ programmer will ever learn. This is the time when I believe that the topic should be broached.
Given C++'s heritage, it would be optimistic
to expect the class
keyword
to be pushed straight to stage 3 of the standing document.
It would also be premature and extreme:
WG21 is not finished with the task of
separating the concerns of classes into features of the IS.
But I believe we should start to question whether class design is still central to the user's ability to write C++ programs. Section 2.3 of the standing document is entitled [delay] Delay features until there is a genuine use-case. I think it is clear that the list of genuine use-cases for writing a class is dwindling.