virtual functions and abstract classesAny class that has a pure virtual function is an abstract class. Pure virtual functions (usually) cannot be called
Shape shape; // Error.
Shape *p = new Shape; // Error.
auto sp = std::make_shared<Shape>(); // Error.
std::shared_ptr<Shape> sp2 = std::make_shared<Rectangle>(p1, p2); // OK.
We can define pointer or reference to an abstract class, but never an object of that type!
virtual functions and abstract classesAn impure virtual function must be defined. Otherwise, the compiler will fail to generate necessary runtime information (the virtual table), which leads to an error.
class X {
virtual void foo(); // Declaration, without a definition
// Even if `foo` is not used, this will lead to an error.
};
Linkage error:
/usr/bin/ld: /tmp/ccV9TNfM.o: in function `main':
a.cpp:(.text+0x1e): undefined reference to `vtable for X'
Is this good?
class Shape {
public:
virtual double area() const {
return 0;
}
};
What about this?
class Shape {
public:
virtual double area() const {
throw std::logic_error{"area() called on Shape!"};
}
};
class Shape {
public:
virtual double area() const {
return 0;
}
};
If Shape::area is called accidentally, the error will happen silently!
class Shape {
public:
virtual double area() const {
throw std::logic_error{"area() called on Shape!"};
}
};
If Shape::area is called accidentally, an exception will be raised.
However, a good design should make errors fail to compile.
[Best practice] If an error can be caught in compile-time, don't leave it until run-time.
Polymorphism: The provision of a single interface to entities of different types, or the use of a single symbol to represent multiple different types.
Run-time polymorphism:
struct Shape {
virtual void draw() const = 0;
};
void drawStuff(const Shape &s) {
s.draw();
}
Compile-time polymorphism:
template <typename T>
concept Shape = requires(const T x) {
x.draw();
};
void drawStuff(Shape const auto &s) {
s.draw();
}
Effective C++ Item 32
By writing that class D publicly inherits from class B, you are telling the compiler (as well as human readers of your code) that
D is also an object of type B, but not vice versa.B represents a more general concept than D, and that D represents a more specialized concept than B.More specifically, you are asserting that anywhere an object of type B can be used, an object of type D can be used just as well.
D, an object of type B won't do.class Person { /* ... */ };
class Student : public Person { /* ... */ };
Every student is a person, but not every person is a student.
Anything that is true of a person is also true of a student:
Something is true of a student, but not true of people in general.
The notion of a person is more general than is that of a student; a student is a specialized type of person.
The is-a relationship: Anywhere an object of type Person can be used, an object of type Student can be used just as well, but not vice versa.
void eat(const Person &p); // Anyone can eat.
void study(const Student &s); // Only students study.
Person p;
Student s;
eat(p); // Fine. `p` is a person.
eat(s); // Fine. `s` is a student, and a student is a person.
study(s); // Fine.
study(p); // Error! `p` isn't a student.
If we naively try to express this in C++, our effort yields:
class Bird {
public:
virtual void fly(); // Birds can fly.
// ...
};
class Penguin : public Bird { // A penguin is a bird.
// ...
};
Penguin p;
p.fly(); // Oh no!! Penguins cannot fly, but this code compiles!
In general, birds have the ability to fly.
Maybe the following hierarchy models the reality much better?
class Bird { /* ... */ };
class FlyingBird : public Bird {
virtual void fly();
};
class Penguin : public Bird { // Not FlyingBird
// ...
};
Maybe the following hierarchy models the reality much better?
class Bird { /* ... */ };
class FlyingBird : public Bird {
virtual void fly();
};
class Penguin : public Bird { // Not FlyingBird
// ...
};
void report_error(const std::string &msg); // defined elsewhere
class Penguin : public Bird {
public:
virtual void fly() {
report_error("Attempt to make a penguin fly!");
}
};
void report_error(const std::string &msg); // defined elsewhere
class Penguin : public Bird {
public:
virtual void fly() { report_error("Attempt to make a penguin fly!"); }
};
No. This does not say "Penguins can't fly." This says "Penguins can fly, but it is an error for them to actually try to do it."
To actually express the constraint "Penguins can't fly", you should prevent the attempt from compiling.
Penguin p;
p.fly(); // This should not compile.
[Best practice] Good interfaces prevent invalid code from compiling.
Should class Square publicly inherit from class Rectangle?
Consider this code.
class Rectangle {
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int getHeight() const;
virtual int getWidth() const;
// ...
};
void makeBigger(Rectangle &r) {
r.setWidth(r.getWidth() + 10);
}
class Square : public Rectangle {
// A square is a rectangle,
// where height == width.
// ...
};
Square s(10); // A 10x10 square.
makeBigger(s); // Oh no!
We said before that the "is-a" relationship means that anywhere an object of type B can be used, an object of type D can be used just as well.
However, something applicable to a rectangle is not applicable to a square!
Effective C++ Item 34
Suppose XYZ has only two kinds of planes: the Model A and the Model B, and both are flown in exactly the same way.
class Airplane {
public:
virtual void fly(const Airport &destination) {
// Default code for flying an airplane to the given destination.
}
};
class ModelA : public Airplane { /* ... */ };
class ModelB : public Airplane { /* ... */ };
Airplane::fly is declared virtual because in principle, different airplanes should be flown in different ways.Airplane::fly is defined, to avoid copy-and-pasting code in the ModelA and ModelB classes.Now suppose that XYZ decides to acquire a new type of airplane, the Model C, which is flown differently from the Model A and the Model B.
XYZ's programmers add the class ModelC to the hierarchy, but forget to redefine the fly function!
class ModelC : public Airplane {
// `fly` is not overridden.
// ...
};
This surely leads to a disaster:
auto pc = std::make_unique<ModelC>();
pc->fly(PVG); // No! Attempts to fly Model C in the Model A/B way!
The problem here is not that Airplane::fly has default behavior, but that ModelC was allowed to inherit that behavior without explicitly saying that it wanted to.
Airplane can fly.ModelC does not override Airplane::fly, it will have the inherited implementation automatically.To sever the connection between the interface of the virtual function and its default implementation:
class Airplane {
public:
virtual void fly(const Airport &destination) = 0; // pure virtual
// ...
protected:
void defaultFly(const Airport &destination) {
// Default code for flying an airplane to the given destination.
}
};
fly provides the interface: Every derived class can fly.defaultFly.If ModelA and ModelB want to adopt the default way of flying, they simply make a call to defaultFly.
class ModelA : public Airplane {
public:
virtual void fly(const Airport &destination) {
defaultFly(destination);
}
// ...
};
class ModelB : public Airplane {
public:
virtual void fly(const Airport &destination) {
defaultFly(destination);
}
// ...
};
For ModelC:
Airplane::fly is pure virtual, ModelC must define its own version of fly.defaultFly.class ModelC : public Airplane {
public:
virtual void fly(const Airport &destination) {
// The "Model C way" of flying.
// Without the definition of this function, `ModelC` remains abstract,
// which does not compile if we create an object of such type.
}
};
Some people object to the idea of having separate functions for providing the interface and the default implementation, such as fly and defaultFly above.
For one thing, it pollutes the class namespace with closely related function names.
Read the rest part of Effective C++ Item 34 for another solution to this problem.
We have come to the conclusion that
Moreover, non-virtual functions specify inheritance of interface + a mandatory implementation.
Note: In public inheritance, interfaces are always inherited.
Pure virtual function and abstract class
= 0.
Public inheritance models the "is-a" relationship.
Inheritance of interface vs inheritance of implementation
ClassName::functionName(args), not via a virtual function call (dynamic binding).
In some cases, we want a class to be made abstract, but it does not have any pure virtual function. A possible workaround is to declare the destructor to be pure virtual, and then provide a definition for it:
struct Foo {
virtual ~Foo() = 0;
};
Foo::~Foo() = default; // Provide a definition outside the class.
The "another solution" mentioned in page 36 is also related to this.