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:

Subsections:

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.

Enforcement

Look for structs with all data private and classes 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

???