C: Classes and Class Hierarchies
A class is a user-defined type, for which a programmer can define the representation, operations, and interfaces. Class hierarchies are used to organize related classes into hierarchical structures.
Class rule summary:
- C.1: Organize related data into structures (
struct
s orclass
es) - C.2: Use
class
if the class has an invariant; usestruct
if the data members can vary independently - C.3: Represent the distinction between an interface and an implementation using a class
- C.4: Make a function a member only if it needs direct access to the representation of a class
- C.5: Place helper functions in the same namespace as the class they support
- C.7: Don't define a class or enum and declare a variable of its type in the same statement
- C.8: use
class
rather thatstruct
if any member is non-public - C.9: minimize exposure of members
Subsections:
- C.concrete: Concrete types
- C.ctor: Constructors, assignments, and destructors
- C.con: Containers and other resource handles
- C.lambdas: Function objects and lambdas
- C.hier: Class hierarchies (OOP)
- C.over: Overloading and overloaded operators
- C.union: Unions
C.1: Organize related data into structures (struct
s or class
es)
Reason
Ease of comprehension. If data is related (for fundamental reasons), that fact should be reflected in code.
Example
void draw(int x, int y, int x2, int y2); // BAD: unnecessary implicit relationships
void draw(Point from, Point to); // better
Note
A simple class without virtual functions implies no space or time overhead.
Note
From a language perspective class
and struct
differ only in the default visibility of their members.
Enforcement
Probably impossible. Maybe a heuristic looking for data items used together is possible.
C.2: Use class
if the class has an invariant; use struct
if the data members can vary independently
Reason
Readability.
Ease of comprehension.
The use of class
alerts the programmer to the need for an invariant.
This is a useful convention.
Note
An invariant is a logical condition for the members of an object that a constructor must establish for the public member functions to assume.
After the invariant is established (typically by a constructor) every member function can be called for the object.
An invariant can be stated informally (e.g., in a comment) or more formally using Expects
.
If all data members can vary independently of each other, no invariant is possible.
Example
struct Pair { // the members can vary independently
string name;
int volume;
};
but:
class Date {
public:
Date(int yy, Month mm, char dd); // validate that {yy, mm, dd} is a valid date and initialize
// ...
private:
int y;
Month m;
char d; // day
};
Note
If a class has any private
data, a user cannot completely initialize an object without the use of a constructor.
Hence, the class definer will provide a constructor and must specify its meaning.
This effectively means the definer need to define an invariant.
- See also define a class with private data as
class
. - See also Prefer to place the interface first in a class.
- See also minimize exposure of members.
- See also Avoid
protected
data.
Enforcement
Look for struct
s with all data private and class
es with public members.
C.3: Represent the distinction between an interface and an implementation using a class
Reason
An explicit distinction between interface and implementation improves readability and simplifies maintenance.
Example
class Date {
// ... some representation ...
public:
Date();
Date(int yy, Month mm, char dd); // validate that {yy, mm, dd} is a valid date and initialize
int day() const;
Month month() const;
// ...
};
For example, we can now change the representation of a Date
without affecting its users (recompilation is likely, though).
Note
Using a class in this way to represent the distinction between interface and implementation is of course not the only way. For example, we can use a set of declarations of freestanding functions in a namespace, an abstract base class, or a template function with concepts to represent an interface. The most important issue is to explicitly distinguish between an interface and its implementation "details." Ideally, and typically, an interface is far more stable than its implementation(s).
Enforcement
???
C.4: Make a function a member only if it needs direct access to the representation of a class
Reason
Less coupling than with member functions, fewer functions that can cause trouble by modifying object state, reduces the number of functions that needs to be modified after a change in representation.
Example
class Date {
// ... relatively small interface ...
};
// helper functions:
Date next_weekday(Date);
bool operator==(Date, Date);
The "helper functions" have no need for direct access to the representation of a Date
.
Note
This rule becomes even better if C++17 gets "uniform function call." ???
Enforcement
Look for member function that do not touch data members directly. The snag is that many member functions that do not need to touch data members directly do.
C.5: Place helper functions in the same namespace as the class they support
Reason
A helper function is a function (usually supplied by the writer of a class) that does not need direct access to the representation of the class, yet is seen as part of the useful interface to the class. Placing them in the same namespace as the class makes their relationship to the class obvious and allows them to be found by argument dependent lookup.
Example
namespace Chrono { // here we keep time-related services
class Time { /* ... */ };
class Date { /* ... */ };
// helper functions:
bool operator==(Date, Date);
Date next_weekday(Date);
// ...
}
Note
This is especially important for overloaded operators.
Enforcement
- Flag global functions taking argument types from a single namespace.
C.7: Don't define a class or enum and declare a variable of its type in the same statement
Reason
Mixing a type definition and the definition of another entity in the same declaration is confusing and unnecessary.
Example; bad
struct Data { /*...*/ } data{ /*...*/ };
Example; good
struct Data { /*...*/ };
Data data{ /*...*/ };
Enforcement
- Flag if the
}
of a class or enumeration definition is not followed by a;
. The;
is missing.
C.8: use class
rather that struct
if any member is non-public
Reason
Readability. To make it clear that something is being hidden/abstracted. This is a useful convention.
Example, bad
struct Date {
int d,m;
Date(int i, Month m);
// ... lots of functions ...
private:
int y; // year
};
There is nothing wrong with this code as far as the C++ language rules are concerned, but nearly everything is wrong from a design perspective. The private data is hidden far from the public data. The data is split in different parts of the class declaration. Different parts of the data has difference access. All of this decreases readability and complicates maintenance.
Note
Prefer to place the interface first in a class see.
Enforcement
Flag classes declared with struct
if there is a private
or public
member.
C.9: minimize exposure of members
Reason
Encapsulation. Information hiding. Minimize the chance of untended access. This simplifies maintenance.
Example
???
Note
Prefer the order public
members before protected
members before private
members see.
Enforcement
???
C.concrete: Concrete types
One ideal for a class is to be a regular type.
That means roughly "behaves like an int
." A concrete type is the simplest kind of class.
A value of regular type can be copied and the result of a copy is an independent object with the same value as the original.
If a concrete type has both =
and ==
, a=b
should result in a == b
being true
.
Concrete classes without assignment and equality can be defined, but they are (and should be) rare.
The C++ built-in types are regular, and so are standard-library classes, such as string
, vector
, and map
.
Concrete types are also often referred to as value types to distinguish them from types uses as part of a hierarchy.
Concrete type rule summary:
C.10 Prefer a concrete type over more complicated classes
Reason
A concrete type is fundamentally simpler than a hierarchy: easier to design, easier to implement, easier to use, easier to reason about, smaller, and faster. You need a reason (use cases) for using a hierarchy.
Example
class Point1 {
int x, y;
// ... operations ...
// ... no virtual functions ...
};
class Point2 {
int x, y;
// ... operations, some virtual ...
virtual ~Point2();
};
void use()
{
Point1 p11 {1, 2}; // make an object on the stack
Point1 p12 {p11}; // a copy
auto p21 = make_unique<Point2>(1, 2); // make an object on the free store
auto p22 = p21.clone(); // make a copy
// ...
}
If a class can be part of a hierarchy, we (in real code if not necessarily in small examples) must manipulate its objects through pointers or references. That implies more memory overhead, more allocations and deallocations, and more run-time overhead to perform the resulting indirections.
Note
Concrete types can be stack allocated and be members of other classes.
Note
The use of indirection is fundamental for run-time polymorphic interfaces. The allocation/deallocation overhead is not (that's just the most common case). We can use a base class as the interface of a scoped object of a derived class. This is done where dynamic allocation is prohibited (e.g. hard real-time) and to provide a stable interface to some kinds of plug-ins.
Enforcement
???
C.11: Make concrete types regular
Reason
Regular types are easier to understand and reason about than types that are not regular (irregularities requires extra effort to understand and use).
Example
struct Bundle {
string name;
vector<Record> vr;
};
bool operator==(const Bundle& a, const Bundle& b) { return a.name == b.name && a.vr == b.vr; }
Bundle b1 { "my bundle", {r1, r2, r3}};
Bundle b2 = b1;
if (!(b1 == b2)) error("impossible!");
b2.name = "the other bundle";
if (b1 == b2) error("No!");
In particular, if a concrete type has an assignment also give it an equals operator so that a=b
implies a == b
.
Enforcement
???