Class Foundations
Concepts, Overview and Syntax of C++ Classes
C++ is an object-oriented language, even if the use of object-oriented features is optional. To implement object-oriented designs using classes, an understanding of the essential language components and syntax specific to C++ is required. This constitutes the minimum syntactical knowledge required before embarking on class templates.
PREREQUISITES — You should already…
- be comfortable with pointers and indirection — and not superficially;
- understand the concepts of lvalues and rvalues;
- be very familiar with lvalue and rvalue references;
- have used, and understand, function signatures and function overloading;
- understand fully the difference between an argument and a parameter;
- be fully aware of the difference between a declaration and a definition;
- understand
inline
functions, constant expressions, and the use ofconst
; - have some experience with the theory and design of object-oriented programs;
- understand, and be using, exception handling.
Overview
The keywords class
and struct
both create
new types. Like enum
, C++ creates
two types, so for ‘struct
class MyType
’, it also creates
‘MyType
’. Syntax-wise, they are equivalent, except that the
default access in a struct
is public
, while
the default access in a class
is private
.
You should generally use struct
as “plain old
data” (POD), i.e., like you would use it in
C99/11. You can use the std::is_pod()
template function from the <type_traits>
meta-programming header, to check if a type complies with the rules for
POD.
We use classes to implement OO (Object-Oriented) designs. This process is called OOAD (Object-Oriented Analysis & Design), and employs a graphical design notation called UML (Unified Modelling Language). The first aspect of OOP (Object-Oriented Programming) involves encapsulation, which, even used by itself, is useful. As an example: Visual Basic, up to Version 6, only supported encapsulation — no inheritance or polymorphism; yet programmers found it an important and distinctly useful feature.
One crucial concept of encapsulation, is that we create a
model for some data, with which we implement an
abstraction that encapsulates the state and
behaviour of the object created from this model. In C++, the
term ‘object’ really just means ‘variable’ but not necessarily one with
a name. We use ‘object’ when we want to emphasise that it is a value of
a class
type. Formally, in OO
terminology, object means “instance of a class”. It is like saying a
house is an instance of a plan — just fancy talk for “the house was
built from this plan”.
Although not enforced, or required, we generally put a class in its
own header file, e.g. file Circle.hpp
would contain the
specification for the Circle
class. Any non-inline
code, or non-template code, must go into a .cpp
file, so
for this class, we would then also have Circle.cpp
, which
we generically refer to as the implementation of the
Circle
class. However, template classes would
often involve just a header file.
IMPORTANT — Related File Inclusion
Whenever this pairing of header and source files exists, whether it
involves classes or not, you absolutely must remember to
#include
the header in the corresponding .cpp
file. Do this even if it does not seem necessary. Doing so will ensure
that there is no mismatch between declarations and definitions.
Otherwise, this would lead to subtle and (very, very) difficult-to-find
bugs.
Access Control
Access control should not be confused with scope. Scope is an area of visibility.
Access control involves permissions. For encapsulation, we only
need to choose between two possibilities private
and
protected
. For inheritance, we also could use
protected
:
private:
— Only code inside the class (member functions) has access permission. This is the default access control setting in aclass
.protected:
— Likeprivate
, with an exception: derived classes' member functions will also have access permissions.public:
— All code, inside or outside of the class, has access permission. This is the default access control setting in astruct
.
If you explicitly set the access control at all times, it does not
syntactically matter whether you use a class
or a
struct
to create your type. But as we said already, use
class
for OOP, and
struct
for simple, C-like structures — simply as a good
coding convention.
Access control specifiers set a level of access for all members that
follow, and it remains in effect until the end of the class, or until
another access control specifier is encountered. Furthermore, you can
repeat an access control specifier, if you desire. For consistency,
either always put private:
members first in the class, or
always place them last — do not switch between these conventions; choose
one.
Inaccessible Members
When a class inherits from a base class, the members that are
private
to the base class, are inherited, like all other
members, but because of access control, they are not accessible
by code in the derived class. Again, the mechanism of inheritance, and
the concepts of access control, involve different semantics — if a class
cannot access members, it does not mean they were not
inherited.
This is an invariant: it remains forever true, regardless of the type
of inheritance employed (public
, private
or
protected
inheritance). As a model then, when inheritance
is involved, we can treat this as a fourth level of access control, even
if it has no formal name. We will refer to such members as having
‘inaccessible’ or ‘hidden’ access.
Class Scope
A type name created with class
or struct
can in some instances be used like a namespace
name. In
other words, it acts like a named scope, and can be used with
the scope resolution operator (qualified
lookup). We use it to qualify members we define (implement)
outside of the class.
member declared in class, defined outside class with scope resolution
class C {
public:
void member (); //←*declare* member.
};//class
···void C::member () { //←qualify name with `C::`,
// otherwise, it would be a
… } // *different* function.
Inside the scope of the class, the scope resolution operator can be used to disambiguate, when two identical names are simultaneously visible.
Logically, when a derived class inherits from a base class, it is in
a nested scope with respect to the base class. Names in the
derived class can thus hide or shadow names, which it
inherited. This is seldom, if ever, a desirable state of affairs. If
such a situation is unavoidable, we can still reach the name in the base
class, from code in the derived class, with the scope resolution
operator:
‘‹base-class-name›::
‹member-name›’.
using scope resolution to disambiguate
class Base {
public:
void member () { } //←`member` function definition.
};//class
class Deriv: public Base {
public:
void member () { //←*hides* inherited `member`.
this->Base::member(); //←qualified lookup of `member`.
::member(); //←qualified lookup of `member`.
Baseif (false) {
this->member(); //←*recursive* (`Deriv::member`).
(); //←*recursive* (`Deriv::member`).
member}
}
};//class
int main () {
using namespace common;
{};
Deriv dob.member(); //←calls `Deriv::member()`.
dob.Base::member(); //←calls `Base::member()`.
dob.Deriv::member(); //←calls `Deriv::member()`.
dob
return EXIT_SUCCESS;
}
It should be clear from the above that the Deriv
class
is abstractly in a nested scope with respect to the
Base
class. Hiding inherited members is seldom a good idea,
but it does help illustrate the concept. Do not confuse “hiding” with
“overloading” or “overriding”.
Note that ‘using namespace common;
’ refers to a
user-defined namespace like:
user-defined namespace used in examples
namespace common {
using std::cout; using std::cin; using std::err; using std::endl;
} // namespace
Data Members
Like a struct
in C, we would have to consider which
data members to put in the class
. The data members
will represent the state of an object defined with this class
type, at any given time. Some refer to this as the “attributes” of the
class. Data members will have abstract meanings (double
or
int
just specifies range, not what it is
used for, or what it means), often inferred from their
names. If a member, for example, is called radius_
and
hopefully means the radius of a circle, then we understand that
a negative value would be invalid. This would make the whole object
invalid.
For data members that must be “shared” by all objects, but logically
belong to the class
, we can use static data members. Data members for
which every new object will get its own copy, are often referred to as
instance data members. Other
terminology that is common, is to refer to data members as “fields” —
for some it means only instance members, and for some it seems to mean
any data member, even symbolic constants created with
constexpr
.
Instance Data Members
Only the class code ‘knows’ what the valid values for any given
instance data member would be, and because we do not want code outside
the class to maliciously, or accidently put invalid values in data
members, they are commonly defined with private:
access.
This could be the beginning of an example Circle
class:
Circle.hpp
— Circle Specification 1
/*!@file Circle.hpp
* @brief Specification of class `Circle`
*/
#if !defined HB449FE120C164DCE94DE1978A9881B4A
#define HB449FE120C164DCE94DE1978A9881B4A
class Circle {
public: // symbolic constants
constexpr static double Pi = 3.14159265359;
private: // data member(s)
double radius_;
};//class
#endif
Note that we use UUIDs (Universally Unique Identifiers) as include guards in header files, as explained on our Preprocessor page.
Apart from variable data members, we can create symbol
constants in the scope of the class. In that respect, the class
will act like a namespace
,
namely that access to Pi
above outside the class, will have
to be in the form: Circle::Pi
.
Although we do not need it yet, we will create the corresponding
Circle.cpp
file as well. This will ensure that we do not
forget later to #include
the header file in the
.cpp
file.
Circle.cpp
— Circle Implementation 1
/*!@file Circle.cpp
* @brief Implementation of class `Circle`
*/
#include "Circle.hpp"
At this point, we can have client code create objects of type
Circle
(but cannot yet do much with it). If the
radius_
data member had public
access, we
could have referenced it, because every variable (object) of type
Circle
, will have an instance of radius_
.
main.cpp
— Circle Client 1
/*!@file main.cpp
* @brief Client program exercising the `Circle` class.
*/
#include "Circle.hpp"
#include <iostream>
int main () {
using std::cout; using std::endl;
{}; //←default ctor supplied implicitly.
Circle c1(c1); //←copy ctor supplied implicitly.
Circle c2= c2; //←assignment operator supplied implicitly.
c1
<< "Pi = " << Circle::Pi << endl;
cout
#if defined SHOW_ERRORS
<< "Pi = " << c1.Pi << endl; //←not legal for `constexpr static` members.
cout << "Radius = " << c1.radius_ << endl; //←`main` not allowed *access*.
cout #endif
return EXIT_SUCCESS;
}
NOTE — Static Member Access
We could have also have accessed Pi
as if an
instance member, with c1.Pi
or c2.Pi
, but only
if we defined Pi
outside the class as: constexpr double Circle::Pi = ···;
.
As coding convention, we avoid accessing members that are shared by all
objects, via the member selection operator — doing so would suggest that
it belongs to that object, which it does not.
Static Data Members
Data members can be declared static
in a class. Conceptually, this means they are shared by all
objects, unlike the other data members, where every object gets its own
instance. Such static
members must be defined in
the implementation file (.cpp
), but without
static
. They can be declared with public:
or
private:
access (but must still be defined in a
.cpp
file, regardless).
A constexpr
definition must be
static
, and can be initialised in the class
,
as we have done with Pi
in the Circle
class.
This obviates the need to define it in the .cpp
file
(unless you really like to access it via the member selection operator
on objects).
myClass.hpp
— Trivial Class Specification 1
/*!@file myClass.hpp
* @brief Trivial Example Class *Specification*.
*/
class myClass {
private:
static int count_; //←*declare* `static` member.
public:
() { ++count_; } //←*define* ctor inline.
myClass~myClass() { --count_; } //←*define* dtor inline.
int count() const { //←*define* `count()` inline.
return count_;
}
};//class
The definition of the variable count_
below, must
not be defined with the static
keyword,
but must still be properly qualified (myClass::…
),
otherwise it would define an altogether different variable.
myClass.cpp
— Trivial Class Implementation 1
/*!@file myClass.cpp
* @brief Trivial Example Class *Implementation*.
*/
#include "myClass.hpp"
int myClass::count_{0}; //←*define* `static` member.
Even though static
class members have a global
lifetime, as is the case with all variables declared
static
, they do not have to be initialised with constant
expressions. This is important, since it leads programmers to start
depending on the order of static
variable
initialisation, which is never good. If not explicitly initialised, they
are guaranteed to be zero-initialised if they
are intrinsic types, or default-initialised
if a user-defined type. They will be initialised before execution enters
main()
.
main.cpp
— Trivial Class Client 1
/*!@file main.cpp
* @brief Trivial Example Class *User/Client*.
*/
#include "myClass.hpp"
#include <iostream>
int main () {
using std::cout; using std::endl;
{}, c2{};
myClass c1
cout<< c1.count() << ' ' //⇒ 2
<< c2.count() << ' ' //⇒ 2
<< '\n';
/* create a new scope
*/ {
{}; //←*hide*/*shadow* higher `c2`.
myClass c2{}; //←another object `c3`.
myClass c3
cout << c1.count() << ' ' //⇒ 4
<< c2.count() << ' ' //⇒ 4
<< c3.count() << ' ' //⇒ 4
<< '\n';
// local `c2` & `c3` destructed.
}
cout<< c1.count() << ' ' //⇒ 2
<< c2.count() << ' ' //⇒ 2
<< endl;
return EXIT_SUCCESS;
}
We used the “shared” count_
data member to keep track of
how many objects are “alive” at any point. This is not a complete
implementation; just enough to illustrate the principle. For a complete
and robust implementation, we would have to increment the
count_
in the other constructors and assignment operators
as well.
NOTE — Count as Static Member
The count()
member function could have been made
static
as well (and for this case, should have),
but we discuss static
member functions later, so we resisted.
Other Data Members
Although we have already shown an example and briefly mentioned it,
data members can be defined with the constexpr
specifier.
This logically creates “symbolic constants”, within the scope of the
class. To actually create them inside the class, you have to make them
static
as well. It is unclear what circumstances warranted
this decision, but so be it. If you want to access them on an object,
as if instance members, you would still have to define
them in an implementation file.
CONST DATA MEMBERS
Data members can also be declared with the const
qualifier. Unlike constexpr
members, they can be
initialised by constructors (and only constructors). This means that
mechanically (at assembler level), they are implemented exactly like
normal instance data members, but the compiler constantly checks that
your code does not modify them. So “const
member” really
means “read-only member”, but definitely not: “symbolic constant”.
MUTABLE DATA MEMBERS
The mutable
qualifier can be added to data members (prefixed). This essentially
means that such a member can be modified, even in a const
or const&
object. This is not common and should be used
judiciously, if at all, but does provide the ability to create objects
that are logically constant, but practically require some minor state
changes.
General Member Functions
In general object-oriented parlance, “methods” are what we in C++ call “member functions”. If you do want to use the term method, at least restrict its use to refer to “instance member function”.
As the client code in main.cpp
above shows, there is not
much we can do with a Circle
object yet. In particular, it
would have been nice to set/get the radius (which is not accessible in
main()
because of the private:
access
specifier). Well, we can write public accessor functions which
main()
can call to get or set the radius. These functions
will be inside the class and will have access to
radius_
.
If the complete definition of a function appears in a function body,
it is automatically treated as if you have defined it with
inline
. You can add the inline
, if you want.
Should you need an inline function's body to appear outside the class,
you must declare the function inline
in the class,
and then define it inline
outside the class (just
remember to properly qualify the scope of the function). To be exact: it
does not have to be declared with an explicit
inline
inside the class, it is just good coding convention
— a reminder to readers that it will be inline
defined later.
Circle.hpp
— Circle Specification 2
/*!@file Circle.hpp
* @brief Specification of class `Circle`
*/
#if !defined H9A20D31965804B089115F1D8020BAE78
#define H9A20D31965804B089115F1D8020BAE78
class Circle {
public: // symbolic constants
constexpr static double Pi = 3.14159265359;
public: // accessor methods.
auto radius() const -> double { return radius_; }
auto setRadius(double radius) -> void;
private: // data member(s)
double radius_;
};//class
#endif
Since (by choice for this example) we did not want
setRadius()
to be inline
, we must put the
definition in the corresponding Circle.cpp
file:
Circle.cpp
— Circle Implementation 2
/*!@file Circle.cpp
* @brief Implementation of class `Circle`
*/
#include "Circle.hpp"
auto Circle::setRadius(double radius) -> void {
if (radius <= 0.0)
throw std::runtime_error("setRadius(): Negative radius!");
radius_ = radius;
}
The term accessor methods is informal — some call them “getters and setters”. It is not a syntax, just the result of a common problem: controlling access to private data in a class. It does not mean we blindly provide accessor methods for each and every data member. It is part of the many design decisions you have to make. Having too many accessor methods, generally results in a loss of abstraction, and hinders re-use.
Also keep in mind there need not be a one-to-one correspondence between accessor methods and backing variables. Accessor methods may indirectly affect disparate data, external data, or nothing at all.
The main.cpp
file can now do more with a
Circle
:
main.cpp
— Circle Client 2
/*!@file main.cpp
* @brief Client program exercising the `Circle` class.
*/
#include "Circle.hpp"
#include <iostream>
int main () {
using std::cout; using std::endl;
{}; //←default ctor supplied implicitly.
Circle c1(c1); //←copy ctor supplied implicitly.
Circle c2
.setRadius(12.34);
c2= c2; //←assignment operator supplied implicitly.
c1
<< "c1 radius = " << c1.radius() << endl;
cout << "Pi = " << Circle::Pi << endl;
cout double area = Circle::Pi * c1.radius() * c1.radius();
<< "c1 area = " << area << endl;
cout
return EXIT_SUCCESS;
}
Clearly, it would be nice to add area()
and
circum()
member functions to the Circle
class,
so that clients like main()
do not have to calculate it
themselves, and can just call c1.area()
, for example:
if area and circum methods were available
int main () {
···<< "c1 area = " << c1.area() << endl;
cout << "c1 circum = " << c1.circum() << endl;
cout ···
We can define them inline
in Circle
,
because they are so small:
area and circum methods added to Circle class
class Circle {
···public: // utility methods
auto area() const -> double { return Pi * radius_ * radius_; }
auto circum() const -> double { return 2.0 * Pi * this->radius_; }//(1)
···};//class
(1)
The ‘this->radius_
’
expression is superfluous, as can be seen by the use of
radius_
inside the area()
function. It is
entirely legal, however, even if verbose, but does serve to remind you
of the this
parameter:
Definition — The ‘this’ Pointer Parameter on Member Functions
Only instance member functions (not static
member
functions), get implicit first parameters called this
,
which is of a pointer to the enclosing class type. When member functions
are called on objects of that type, the compiler automatically passes
the address of the object as first argument. When members are referenced
inside such a function, the compiler automatically prefixes
‘this->
’. We sometimes use it explicitly to
disambiguate, or as *this
(‘indirect this
’) to
return, or explicitly pass to another (non-member) function.
Const Member Functions
Member function headers can have the const
qualifier appended. We call them “const
member
functions”. This has two implications:
- The compiler will check that you do not modify any data members in such a function.
- You can call a
const
member function, on aconst
(orconst&
) object. This is not true for non-const
member functions.
It is therefore important that you are aware of your design. Whenever
you write a function which, by design, must not modify members, you
should mark it const
.
Since the const
is part of the function header
(prototype, if you like), it participates in the signature of
the function. The following two member functions are consider to have
different signatures. The compiler will automatically match the
non-const
version, when selected on a
non-const
object, and the const
version, if
selected on a const
(or const&
)
object.
const member functions vs non-const member functions
class C {
public:
void MF() { std::cout << "non const object" << std::endl; }
void MF() const { std::cout << "const object" << std::endl; }
};//class
···{};
C cconst C& r{c};
const C* p{&c};
.MF() // calls `C::MF()`.
c.MF() // calls `C::MF() const`.
r->MF() // calls `C::MF() const`. p
This feature is especially useful when we write functions that return
references to members or elements, like the overloaded subscript
operator. Otherwise, we would not be able to use something like
subscript on a const
object.
NOTE — The “noexcept” Specifier
The noexcept
specifier after a function header, is independent of a
const
specifier being present, or not. Unlike
const
, it can be used on any function, not just
member functions. It promises that the function will not throw an
exception, or cause an exception to escape it.
Static Member Functions
Member functions can also be static
,
as we will show, and we call them “static
member
functions”. Not only that, but they can be implicitly defined
inline
inside the class, so there would be no need to
define them in the implementation file. If such a function was only
declared, however, we would still have to define it somewhere,
like in the implementation file (.cpp
file). The
definition, as with the variable count_
below, must
not be defined with the static
keyword,
but must still be properly qualified (myClass::…
).
Non-static
member functions, by virtue of the
this
pointer parameter, behave as if each object has its
own instance of every member function. This is why they are sometimes
referred to as instance member functions (or methods, if you
like). A member function is declared static
,
or defined static
(implicitly inline
), or
defined inline static
. If the definition must go
in the implementation .cpp
file, the static
storage class specifier must be omitted in the definition.
C.hpp
— Trivial Class C Specification
/*!@file C.hpp
* @brief Trivial Class C Specification
*/
#if !defined H04495F7BDAF54EC0863704E834CDB23A
#define H04495F7BDAF54EC0863704E834CDB23A
class C {
public:
inline static int SF1 () { return 111; } //←`inline` optional.
inline static int SF2 (); //←needs definition.
static int SF3 (); //←needs definition.
};
inline int C::SF2() { return 222; }
#endif
NOTE — Optional “inline”
Explicitly using inline
for the static
function SF1()
is optional, just like with instance member
functions — if the body of a function is in the class, i.e., it is
defined in the class, it is automatically inline
.
It is therefore a matter of preference and convention. It is also
optional for SF2()
, but again, we consider it good coding
convention.
C.cpp
— Trivial Class C Implementation
/*!@file C.cpp
* @brief Trivial Class C Implementation
*/
#include "C.hpp"
int C::SF3() { return 333; }
A client of the class C
can now either create objects of
type C
, or can directly use static
members,
without any object of type C
being present in the
expression.
main.cpp
— Trivial Class Client
/*!@file main.cpp
* @brief Trivial Class C User/Client
*/
#include "C.hpp"
#include <iostream>
int main () {
using std::cout; using std::endl;
// use scope resolution to call functions.
cout << C::SF1() << ", " << C::SF2() << ", " << C::SF3()
<< endl;
{};
C ob// use member selection on object to call functions.
cout << ob.SF1() << ", " << ob.SF2() << ", " << ob.SF3()
<< endl;
return EXIT_SUCCESS;
}
As you may have observed, static
functions, like the static
data
members, can be accessed either via the class name and scope
resolution operator, or by member selection on an object as if
they are instance methods. Unless you have some strong conviction in
this regard, we suggest that you access them only via the class.
The previous example that maintained a count_
of “alive”
objects, can be rewritten to use a static
count()
function to return the number of objects, instead
of the instance method used before:
myClass.hpp
— Trivial Class Specification 2
/*!@file myClass.hpp
* @brief Trivial Example Class *Specification*.
*/
class myClass {
private:
static int count_; //←*declare* `static` member.
public:
() { ++count_; } //←*define* ctor inline.
myClass~myClass() { --count_; } //←*define* dtor inline.
static int count() { //←*define* `count()` `static`,
return count_; //←and (implicitly) `inline`.
}
};//class
The definition of the variable count_
below, must
not be defined with the static
keyword,
but must still be properly qualified (myClass::…
).
myClass.cpp
— Trivial Class Implementation 2
/*!@file myClass.cpp
* @brief Trivial Example Class *Implementation*.
*/
#include "myClass.hpp" //←no need to *define* `count()`.
int myClass::count_{0}; //←*define* `static` member.
Not much has changed for main()
, except we have the
option now to directly call the count()
function by
utilising the scope resolution operator.
main.cpp
— Trivial Class Client 2
/*!@file main.cpp
* @brief Trivial Example Class *User/Client*.
*/
#include "myClass.hpp"
#include <iostream>
int main () {
using std::cout; using std::endl;
{}, c2{};
myClass c1
cout<< c1.count() << ' ' //⇒ 2
<< c2.count() << ' ' //⇒ 2
<< myClass::count() //⇒ 2 ←added
<< '\n';
/* create a new scope
*/ {
{}; //←*hide* higher `c2`.
myClass c2{}; //←another object `c3`.
myClass c3
cout << c1.count() << ' ' //⇒ 4
<< c2.count() << ' ' //⇒ 4
<< c3.count() << ' ' //⇒ 4
<< myClass::count() //⇒ 4 ←added
<< '\n';
// local `c2` & `c3` destructed.
}
cout<< c1.count() << ' ' //⇒ 2
<< c2.count() << ' ' //⇒ 2
<< myClass::count() //⇒ 2 ←added
<< endl;
return EXIT_SUCCESS;
}
Again, we can access a static
member, in this case, a
static
method, either via the class name, (scope
resolution), or as if it is an instance method. We prefer the
former, but used both versions above as illustration.
Friend Functions
The friend
declarator can be used to declare normal
non-member functions in a class
.
You can think of the class “extending or granting friendship” to some
arbitrary function. The effect will be that such “friend” functions will
have permissions to access private members. Of course, since such
functions will not have a this
pointer parameter, you must
pass objects of the friendship-granting class to them. This should be
done with care, not arbitrarily.
One legitimate reason why we need friend
functions, is
when we need to control the first parameter, which is automatically the
this
pointer in instance member functions. For example, if
you want to overload the insertion operator
(operator<<()
), and you want it to work as expected
(that it can be chained), the first parameter must be of type
ostream&
. Then you simply write it as a normal operator
overloading function, and declare it as a friend
in the
class. We do that with Full Circle.
All the member functions of a foreign class can be turned into
friend
functions, simply by declaring the whole class a
friend
: friend class Foreign;
inside the
friend-granting class. This should only be used when you have a good
reason, and not as a result of bad design.
Special Class Members
The C++11 compiler will provide (synthesise) some member functions in
a class
or struct
under certain circumstances.
These special members always require special consideration — they cannot
just be ignored, or trivially implemented, or the synthesized versions
simply accepted.
Canonical List
Since we do not have that many keywords in C++, the semantics depend
a lot on patterns. The following patterns are related to the
special members of a class C
.
C()
— default constructor. It can be implicitly invoked, or explicitly invoked with the new uniform initialisation syntax, consisting of an empty set of curly braces:C
‹ident›{};
.~C()
— destructor. This is seldom directly invoked, but can be. Mostly we appreciate, and depend with great certainty, that it will be called the moment an object goes out of scope (when the trail of execution is just about to enter a higher level scope).C(const C&)
— copy constructor. The copy constructor is called when you initialise an object with another object; when you pass an object by value; and when youreturn
an object. You can call it directly to make a temporary copy of an object.C& operator=(const C&)
— copy assignment. The assignment operator is called when you assign one object to another. It should guard against self-assignment.C(C&&)
— move constructor. This is a performance feature that avoids redundant copies. In most cases, the compiler will determine when it is convenient to call it, for example, when returning an object by value from a function. It only works with an rvalue as parameter.C& operator=(C&&)
— move assignment. The compiler will automatically prefer this version if the class implements it, and the right-hand side expression is an rvalue. It also aids performance.
The C
above is representative of any class name.
Implicit Compiler-Provided
The rules for which special members are synthesised, depending on
which of the special members are user-declared, cannot be summarised in
one sentence. The table below shows what happens when the programmer
explicitly declares (user-declare) a member in the leftmost column,
starting with nothing (not declaring, or using
=delete
, or using =default
on any). We used
abbreviations to make the table manageable: ctor
≡
constructor, dtor
≡ destructor,
dft
≡ default, decl
≡
declared and asgn
≡ assignment
operator.
User… | dft ctor | copy ctor | copy asgn | dtor | move ctor | move asgn |
---|---|---|---|---|---|---|
nothing | default | default | default | default | default | default |
dft ctor | ··· | default | default | default | default | default |
copy ctor | not decl | ··· | default | not decl | not decl | not decl |
copy asgn | default | default | ··· | default | not decl | not decl |
dtor | default | default | default | ··· | not decl | not decl |
move ctor | not decl | deleted | deleted | default | ··· | not decl |
move asgn | default | deleted | deleted | default | not decl | ··· |
other ctor | not decl | default | default | default | default | default |
default — It is deprecated to not supply the other
members, when you declare any of the first four. In other words, either
supply none of the first four, or all of them (even if you make some
‘=default
’).
GUIDELINE — Rule of 3 & 5
When a class is non-trivial enough to warrant any constructor,
destructor, or an overloaded assignment operator, always provide, at
least, all four of: default constructor, copy constructor, destructor,
and overloaded copy assignment operator, even if you declare them with
‘=default
’. Ideally, also declare the other two: move
constructor and overloaded move assignment operator. It is generally
called the “rule of three”, or from C++11, “rule of five”.
Defaulted members are only actually created when
used. So you pay no penalty for declaring a
member ‘=default
’, and then never using it — but then it
should probably have rather been ‘=delete
’.
Default Move Members
This is pseudocode for what the compiler does, when it synthesises a move constructor:
synthesized move constructor pseudocode
class Deriv
: public Base {
private:
data_;
T public:
(Deriv&& rhs)
Deriv : Base(static_cast<Base&&>(rhs))
, data_(static_cast<T&&>(rhs.data_)) {
// when you write this yourself, make sure to
// let `rhs` *release* (disown) resources.
}
};//class
By the way, static_cast<T&&>(E)
casts
E
to an rvalue reference, so we may as well replace the
above with std::move(…)
from the <utility>
header. It is
imperative to set rhs
to not delete resources,
only release (disown) them (or, in other words, “set to
resourceless state”).
This is pseudo code for what the compiler does, when it synthesises a move assignment operator:
synthesized move assignment operator pseudocode
class Deriv
: public Base {
private:
data_;
T public:
& operator= (Deriv&& rhs) {
Deriv::operator= (static_cast<Base&&>(rhs));
Basedata_ = static_cast<T&&>(rhs.data_);
// when you write this yourself, make sure to
// let `rhs` *release* (disown) resources.
return *this;
}
};//class
Again, instead of static_cast<…>
, we could have
used std::move(…)
. Also,
rhs
must not own any resources any more (resourceless
state). There are no rules that govern what can be done with the
rhs
object (“moved from object”) — that is determined by invariants imposed by the designer of the class.
For example, with std::vector
, you are guaranteed
that size()
will
return 0
.
Never write special members in terms of one another. Move members are only about performance. For example, the popular swap idiom can be used to provide exception safety for an overloaded assignment operator, but that means it is implemented in terms of the copy constructor.
Initialisation
Objects are initialised when they come into scope, at the point they are defined. Even if the initialisation syntax involves the assignment character, that is not the assignment operator — it is still initialisation, or as we prefer to call it in C++: construction.
In C++11 and up, variables and members can be initialised. The term ‘initialisation’,
depending on context, may refer to a concept describing
behaviour, and a syntax. The syntax used determines the
behavioural mechanisms employed by the compiler. Do remember though,
that the context of a syntax, may change its semantics and thus
initialisation behaviour (e.g. the same syntax is used to define
variables on the external level, on the internal level, and inside
class
or struct
, but the initialisation rules,
scopes and lifetimes are different).
Kinds of Initialisation
Here we discuss three possibilities, all formally defined, that concern initialisation when no specific value is given. This therefore does not involve specific constructors, or the copy constructor, but the default constructor may come into play, depending on the ‹type› of the ‹obj› (variable):
- ‹obj› is default-initialised — ‹type›
‹obj›
;
, ornew
‹type›;
- If <type> is an intrinsic,
enum
or pointer type (scalar), no initialisation is performed, leading to indeterminate values. - If ‹type› is a
class
orstruct
, its default constructor is called. - For C-arrays of ‹type›, each element is default-initialised.
- If <type> is an intrinsic,
- ‹obj› is zero-initialised — filled with
0
s on global lifetime and thread-local objects, before (potential) further initialisation:- If ‹type› is an intrinsic,
enum
or pointer type (scalar), ‹obj› is zero-initialised. - If ‹type› is a
class
orstruct
, all base classes and ‹type› members are zero-initialised. - For C-arrays of ‹type›, each element is zero-initialised.
- If ‹type› is an intrinsic,
- ‹obj› is value-initialised
- If ‹type› is not an intrinsic,
enum
or pointer type (scalar), ‹obj› is zero-initialised. - If ‹type› is a
class
orstruct
, it is default-initialised, after zero-initialisation, if ‹type›'s default constructor is not provided or=delete
d. - For C-arrays of ‹type›, each element is value-initialised.
- If ‹type› is not an intrinsic,
initialisation examples and comments
// external level:
//
; //←zero-initialised → default-initialised.
T EVstatic T ES; //←zero-initialised → default-initialised.
// internal level:
{
(); //←function declaration.
T X; //←default-initialised.
T I{}; //←value-initialised.
T J{T()}; T L2{T{}}; //←value-initialised.
T K
* P;
T= new T; //←default-initialised.
P = new T(); //←value-initialised.
P = new T{}; //←value-initialised.
P }
struct S1 {
m_; S1() : m_() { } //←`m_` value-initialised.
T };
struct S2 {
m_; S2() : m_{} { } //←`m_` value-initialised.
T };
struct S3 {
m_; S3() { } //←`m_` default-initialised.
T };
This is not to be confused with direct-initialisation, which involves constructors or initialisers with specific values, but excludes copy-initialisation, which is one of the special functions. C++14 also slightly changes some rules regarding constant-initialisation, but still works intuitively as most would expect.
GUIDELINE — Deprecated C++14 Syntax
The syntax for initialisation in the form of:
‹type› ‹ident› = {
‹expr› };
is deprecated from C++14 onwards, so use:
‹ident› {
‹expr› };
or
‹ident› (
‹expr› );
instead.
Constructors
One important feature missing from Circle
is the ability
to initialise it with a radius. The implicit constructors that the
compiler supplied, are not enough. As a reminder: if you do not provide
any constructor, the compiler supplies the following, shown here as
patterns, and assuming a class C
:
C()
default constructor.C(const C&)
copy constructor.C(C&&)
move constructor.C& operator=(const C&)
overloaded copy assignment operator.C& operator=(C&&)
overloaded move assignment operator.
The rules are a bit complicated, but for non-trivial classes, we suggest you either write all of them, or you write some of them, and specify which ones must use the compiler-supplied versions. If you provide a non-special specific constructor, the compiler will supply all the default members, except for the default constructor.
explicitly specifying defaults for constructors and assignment
class Circle {
···public: // ctors & overloaded assignment
() = default;
Circle(const Circle&) = default;
Circle(Circle&&) = default;
Circle& operator= (const Circle&) = default;
Circle& operator= (Circle&&) = default;
Circle
(double radius)
Circle: radius_{radius} { }
// ↑_______________↑ ctor member initialiser list
};//class
The C++11 ‘… = default;
’ statements above simply state,
explicitly, that we are happy with the default
implementations the compiler normally provides, if we do not write
our own.
The last constructor is the one we must provide explicitly. We used a
syntax called “constructor member initialiser
lists”, which consists of a :
between the header and
the body, followed by a list of data member names, each with a
parentheses-delimited, or a brace-delimited, initialiser. Now we can
explicitly initialise a new Circle
variable with a radius,
if we want:
calling various Circle constructors
int main () {
(1.2); //← or: `Circle c1(1.2);`, or `Circle c1 = 1.2;`
Circle c1double radius = 1.2;
(radius); //← any `double` will do, not just a literal.
Circle c2= 12.34; //← same as: `c1 = Circle(12.34);` or:
c1 // `c1 = static_cast<Circle>(12.34);` …
Constructors can be called explicitly. This will create a temporary
object, which can be copied, assigned, or returned. Constructors can
also delegate
(indirectly call) another constructor to perform some, or all, of the
initialisation work. For example, we could modify the
Circle
's default constructor, to delegate to the
constructor that takes a double
as parameter:
constructor delegation
class Circle {
···public: // ···
() : Circle(1.0) { }
Circle
···};//class
Remember that constructors can be overloaded, that they are instance
member functions, and that they also have a this
parameter,
just like normal instance member functions. Under very special
circumstances (like requiring literal
types), a constructor can be marked as a constexpr
function. (You can test if a type T
is a literal type with
is_literal_type
from the <type_traits>
header).
Terminology — A ‘constexpr’ Class
A class where all the members have been defined
constexpr
, is called a constexpr
class, which some people refer to as a literal class.
The copy constructor is especially important, since it is automatically called, more times than you may imagine. It is implicitly called:
- when passing an object by value;
- when returning an object by value.
If you call it explicitly, it creates a temporary object, in which case the compiler may use the move constructor or move assignment operator to “copy” the object, depending on what the rest of the expression attempts to do with the temporary object.
In C++11 and up, you generally do not worry about the cost of copy
construction. At least not for well-designed classes like those in the
C++ standard library. You should have no qualms to return large
containers by value from functions, due to the magic of move semantics.
If the compiler cannot determine whether it should call the cheaper move
constructor, and you know it is safe, you can cast an object to an
rvalue reference with the std::move()
function
— this will effectively force the compiler to use the move constructor
or move assignment operator.
IMPORTANT — Implicit Cast Constructor
Any constructor, that takes one argument of a different type than the
class, is implicitly a cast operator. It can even be
called with either the C-style cast operator, or the named
static_cast<>
operator. If you do not want this to be
used implicitly (and generally, you do not), use the
explicit
specifier in front of all such constructors.
Destructors
Together with appropriate use of the constructors, destructors allow us to implement the resource acquisition is initialisation (RAII) idiom, which can be used in languages with deterministic destruction. Destructors can be called explicitly, but are guaranteed to be called the very moment a variable of a user-defined type goes out of scope. If the default destructor is present in lieu of a specific destructor, it will be called instead.
We do not need a destructor in the Circle
class; it is
just too simple to warrant one. But, we can still add one, just to
illustrate the syntax:
simple destructor syntax example
class Circle {
···public: // destructor
~Circle() { }
···};//class
This destructor does nothing, but in a non-trivial class, it would
perform clean-up, like delete
ing memory allocated in the
constructors. Syntactically, you can call a destructor explicitly, but
that is not common. Instead, we depend on the compiler's guarantee that
it will call a destructor of any object that goes out of scope
— the very moment it goes out of scope. We call this
deterministic destruction. Of course, we could just as well
have asked for the default constructor explicitly with:
explicitly specifying synthesized destructor
~Circle() = default;
One of the times we may need to explicitly call a destructor, is when
we have used placement new
to
initialise an object. Placement new
does not allocate
memory, it only initialises given memory, calling a constructor
determined by the caller. Calling delete
on this memory,
will very likely result in memory corruption; leaving us with the need
to manually destruct the memory — call the destructor explicitly.
In-Class Data Member Initialisation
From C++11 onwards, it is legal to initialise instance data members
inside the class (where you define them). This initialisation is
runtime code, and is not performed during compilation of the
class
or struct
. Rather, the compiler
“remembers” the initialisation, and will perform such initialisation on
all members before a constructor is called, but only as a
consequence of a constructor call. If the constructor in
question explicitly initialises a member that has been defined with
in-class initialisation, this initialisation will not be
performed.
The initialisation expression is a runtime expression, which means it can even be a function call.
icinit.cpp
— In-Class Member Initialisation
/*!@file icinit.cpp
* @brief In-Class Member Initialisation Example
*/
#include <iostream>
class C {
private:
long d1_; //←no in-class initialisation.
int d2_{123}; //←in-class initialisation.
double d3_{init()}; //←in-class initialisation.
static
double init() { return 4.56; }
public:
() { } // `d1_←?`, `d2_←123`, `d3_←4.56`.
C (int d2)
C : d1_{}, d2_(d2) // `d1_←0`, `d2_←d2`, `d3_←4.56`.
{ }
(double d3)
C : d1_(111), d3_(d3) // `d1_←111`, `d2_←123`, `d3_←d3`.
{ }
void dump() {
std::cout
<< "d1_=" << d1_ << ", "
<< "d2_=" << d2_ << ", "
<< "d3_=" << d3_ << std::endl;
}
};//class
#define L do{ cout << "c" << __LINE__ << ": "; }while(0);
int main () {
using std::cout; using std::endl;
; C c2{}; C c3(111); C c4(222.333);
C c1# line 1
.dump(); //←`d1_` has undefined value.
L c1.dump(); //←`d1_` has undefined value.
L c2.dump(); //←`d1_` default-initialised.
L c3.dump();
L c4
return EXIT_SUCCESS;
}
The way to understand what is happening, it to think of the compiler's process as follows: when the compiler must construct a new object (i.e., a constructor is called, explicitly, or implicitly), it must consider the initialisation of each member, in the order their definitions / declarations have appeared in the class. So for each member it will:
if the constructor mentions it in its initialiser list, perform that initialisation;
else if the member is not in the initialiser list, but it has in-class initialisation, then perform that initialisation;
else default-initialise the member (if the member has an intrinsic type, this means it will not do anything, but for user-defined types, it will perform default-initialisation).
You can alternatively think that the compiler must create an
initialiser list; either implicitly, or use your overrides, if provided.
This means that for the class C
above, d1_
is
always considered first, then d2_
and then
d3_
. For each constructor, it must consider what to do with
each of them in that order:
- use member initialiser list entry, if it exists, or
- use an in-class initialiser, if it exists, or
- do default-initialisation.
SAMPLE RUN & OUTPUT
$> g++ -Wall -Wextra -std=c++14 -O3 -DNDEBUG -o icinit picinit.cpp; ./icinit
c1: d1_=4508508745, d2_=123, d3_=4.56
c2: d1_=140734683879296, d2_=123, d3_=4.56
c3: d1_=0, d2_=111, d3_=4.56
c4: d1_=111, d2_=123, d3_=222.333
If you compile and run the same program, you will get different
values for c1.d1_
and c2.d1_
, because the
default constructor used for c1
and c2
, did
not explicitly initialise d1_
in its member initialiser
list, and d1_
has an intrinsic type.
It gets more complicated if you specified the default constructor as:
C() = default;
.
In this case, the value of c2.d1_
would have been
0
, guaranteed, but the value of c1.d1_
would
still have been indeterminate. That is because a different kind of
mechanism is used if you define a variable without any
constructor: C c1;
versus:
C c2{};
.
IMPORTANT — In-Class Member / Field Initialisation
Use in-class initialisers wherever you can. It will cause fewer surprises, and will be performed for every constructor, reducing redundancy… and will be efficient, since it gives precedence to members initialised in the constructor.
Overloading Operators
Operators, as we mentioned from the start, act abstractly like functions. C++ allows us to actually leverage that abstraction and make it a reality. Apart from operators on the fundamental types, any operator notation can be rewritten in terms of function calls. The function call can either be a non-member function, or a member function — the compiler will look for either. Some can always be just be overloaded as member functions.
Although we have shown overloaded versions of the assignment and insertion operators, they have a particular pattern, and are easy to use as templates for implementation with other classes. But there are some concepts and rules that are required, in order to generalise your knowledge.
The other oddity, is that the names of operator functions start with
the operator
keyword, followed by the operator symbol.
Whitespace between the operator
keyword and the symbol is
not syntactically significant; nor is whitespace after the symbol.
+ B ≡ operator+(A,B) ‖ A + B ≡ A.operator+(B)
A * B ≡ operator*(A,B) ‖ A * B ≡ A.operator*(B)
A << B ≡ operator<<(A,B) A
Since the last example is normally used as the insertion operator, we did not show its alternative option as a member function, since it would not have been possible to get a stream as the first parameter, which is a requirement for chaining insertion operators. But you could; you just shouldn't.
The point is that, whether you write A+B
or
operator+(A,B)
, is a choice of notation — both have the
same result, and both are legal. Looking at a more complex expression in
operator notation, and converting it to function calls, should
illuminate the benefits of operators:
+ B * C - D ≡ operator-(operator+(A, operator*(B, C)), D) A
We do not even show what it would look like if called as member functions!
Most operators can be overloaded. Some operators must be overloaded as member functions inside a class. The term “operator overloading” is really just the same mechanism as normal function overloading — entirely based on signatures. It is made possible because of an independent mechanism, namely the treatment of operators as function calls, albeit functions with “strange” names.
Basic Syntax
Operators that are overloadable, are functions with one of
these patterns, where ▢
indicates one of the operator
characters, P indicates parameters, and T indicates
any valid return type. Whitespace between and the operator
keyword, or the left parenthesis, is allowed, and not significant and
does not contribute to the semantics.
- T
operator
(
P)
Unary and binary operators. T is return type.- T
operator++(int)
Unary postfix increment (as member). - T
operator--(int)
Unary postfix decrement (as member). - T
operator++(
type ident, int)
Unary postfix increment (as non-member). - T
operator--(
type ident, int)
Unary postfix decrement (as non-member).
- T
operator
T(
P)
Type cast to T operator. Note that no return type is allowed.void* operator new (size_t)
Single type dynamic memory allocator.void* operator new[] (size_t)
Array type dynamic memory allocator.- T
operator"" (
P)
User-defined literal suffixes operator (C++11).
The …operator new…
actually has many variations; only the
basic versions are shown. The same applies to
…operator delete…
.
NOTE — Cast to Class Type
To cast from a type T
to a C
(class) type,
we need to write a constructor that takes one argument of type
T
: C(const T& rhs) { … }
. Of course, using
const
or &
is not relevant, just common —
the point is, it can accept a T
type value.
Overloadable Operators
Possible values for above: +
(unary and binary),
-
(unary and binary), *
(multiplication and
indirection), /
, %
, ^
,
&
, |
, ~
, !
,
=
, <
, >
,
<=
, >=
, ==
,
!=
, +=
, -=
, *=
,
/=
, %=
, ^=
, &=
,
|=
, <<
, >>
,
&&
, ||
, ++
(prefix and
postfix), --
(prefix and postfix), ,
(comma),
->*
, ->
, ()
(function
call), []
(subscript).
The post in/decrement operators must be overloaded with a special syntax:
post-increment vs pre-increment operator overloading
class C {
int c_;
public:
const int operator++() { return ++c_; } //←prefix.
const int operator++(int) { //←postfix.
int tmp{c_}; //←implement in terms
++c_; // of pre-increment.
return tmp;
}
};//class
TIP — Pre/Post-Increment/Decrement Operators
Especially in regard to overloaded increment operators, you should prefer the pre-increment operator. If it was not for compiler optimisation, you can almost be guaranteed that the post-increment operator will be slower. This applies to the decrement operators as well. They differ only in their return value, so if you do not use the return value, it conceptually does not matter which you use; from an efficiency perspective however, it does matter.
The operators that can only be overloaded as member functions:
=
— assignment, []
—
subscript, ()
— function call.
Assignment Considerations
The assignment operators are of two types: “copy assignment” or “move assignment”, each with a specific signature:
- copy assignment —
T& operator= (const T& rhs)
- move assignment —
T const& operator= (T&& rhs)
If you implement the exception-safe swap idiom for your assignment, the parameter must be passed by value, since “moving” a value also means modifying the “moved-from” object.
- copy assign with
swap —
T& operator= (T rhs)
With this version of an assignment operator, it means that it
actually employs the copy constructor (pass by value), and a custom
swap()
or std::swap
function,
making it dependent on the copy constructor, and will be slightly slower
than an alternative version. But you do get exception safety, if that is what
you require. Or, you can overload the assignment operator as normal (as
fast as possible), and give users a safe_assign()
function
to use, for when they want an exception guarantee.
The following example is a bit long, if we were only to consider the difference between move assignment and copy assignment. But this does put it in a more realistic context, since in real code, we must also consider the copy constructor and the move constructor. This is still not a complete, production-ready class, but it has all the “good bits” we can discuss later:
copymove.cpp
— Move Semantics
/*!@file copymove.cpp
* @brief Move Semantics Example
*/
#include <iostream>
#include <algorithm>
class CC {
public: // ctors & dtor.
() : data_{}, size_{} {
CC std::cout << " CC()[" << size_ << "] ";
}
(const CC& rhs) //←copy ctor.
CC : data_{ rhs.size_ ? new int[rhs.size_] : nullptr }
, size_{rhs.size_} {
std::copy(rhs.data_, rhs.data_ + rhs.size_, data_);
std::cout << " CC(const CC&)[" << size_ << "] ";
}
(CC&& rhs) noexcept //←move ctor.
CC : data_{std::move(rhs.data_)}
, size_{std::move(rhs.size_)} {
.data_ = nullptr; rhs.size_ = 0U;
rhsstd::cout << " CC(CC&&)[" << size_ << "] ";
}
explicit CC (size_t size) //←specific/custom ctor.
: data_{ size ? new int[size]{} : nullptr } , size_{size} {
std::cout << " CC(size_t)[" << size_ << "] ";
}
~CC () { //←dtor.
std::cout << " ~CC(" << size_ << ") ";
delete[] data_; data_ = nullptr; size_ = 0U;
}
public: // overloaded operators
& operator= (CC& rhs) { //←copy assignment.
CCstd::cout
<< " operator=(CC&)[" << size_ << "<-" << rhs.size_ << "] ";
if (this != &rhs) { //←not really necessary.
{rhs};
CC tmpstd::swap(data_, tmp.data_);
std::swap(size_, tmp.size_);
}
return *this;
}
& operator= (CC&& rhs) //←move assignment.
CCnoexcept {
std::cout
<< " operator=(CC&&)[" << size_ << "<-" << rhs.size_ << "] ";
data_ = std::move(rhs.data_); rhs.data_ = nullptr;
size_ = std::move(rhs.size_); rhs.size_ = 0U;
return *this;
}
public: // methods
size_t count () const noexcept { return size_; }
private:
int* data_; size_t size_;
};//class
() { //←return a `CC` obj.
CC rfunc std::cout << " rfunc()->44 ";
return CC{44};
}
void pfunc (CC r) { //←`CC` obj. pass-by-value.
std::cout << " pfunc(" <<r.count() << ") ";
}
#define NL std::cout << std::endl << __LINE__ << ": ";
int main () {
using std::cout; using std::endl;
#line 1
(11); //⇒ 1: size_t ctor.
NL CC r1(r1); //⇒ 2: copy ctor.
NL CC r2{}; //⇒ 3: default ctor.
NL CC r3= CC(33); //⇒ 4: size_t ctor & move assign.
NL r1 = r1; //⇒ 5: copy assign.
NL r2 = std::move(r1); //⇒ 6: cast & move assign.
NL r3 (r2); //⇒ 7: copy ctor.
NL pfunc(std::move(r3)); //⇒ 8: move ctor.
NL pfunc= rfunc(); //⇒ 7: size_t ctor & move assign.
NL r1 << "\n" << endl;
cout
return EXIT_SUCCESS;
}
SAMPLE RUN & OUTPUT
$ g++ -Wall -Wextra -std=c++14 -O3 -DNDEBUG -o cpmv copymove.cpp; ./cpmv
1: CC(size_t)[11]
2: CC(const CC&)[11]
3: CC()[0]
4: CC(size_t)[33] operator=(CC&&)[11<-33] ~CC(0)
5: operator=(CC&)[11<-33] CC(const CC&)[33] ~CC(11)
6: operator=(CC&&)[0<-33]
7: CC(const CC&)[33] pfunc(33) ~CC(33)
8: CC(CC&&)[33] pfunc(33) ~CC(33)
9: rfunc()->44 CC(size_t)[44] operator=(CC&&)[0<-44] ~CC(0)
~CC(0) ~CC(33) ~CC(44)
The first point of interest, is 4:
, where we create a
temporary (rvalue), and the compiler automatically used the
move assignment operator. The destructor had nothing to do, since the
original size_t
in r3
was 0
, but
the point is, the destructor was called on the old value of
r3
.
The second interesting line, is 6:
. The std::move
function
casts the lvalue (r1
) to an rvalue, causing the
compiler to select the move assignment operator. Since r1
is not yet out of scope, its destructor is not yet called.
On lines 7:
and 8:
, we call
pfunc()
; once with an lvalue, which causes the copy
constructor to be called; and the second time, with an rvalue, which
causes the compiler to choose the move constructor instead. When the
function returns, r3
is invalid (it has been
“moved-from”).
On line 9:
we witness that the compiler recognises that
a function return is a temporary (rvalue), and will call the move
assignment to “move” it to r1
, instead of the normal copy
assignment.
So, everywhere you see &&
in the output, you
have achieved a performance enhancement, which is only possible from
C++11 onwards, thanks to rvalue references, move
constructors, move assignment
and operator
overloading, all together, giving us “move semantics”.
We did not implement the copy-and-swap idiom, since that would make the move assignment operator superfluous and thus we would not have been able to discuss it. If you do want to implement it, replace both assignment operator overloads with this single version:
copy-and-swap idiom alternative assignment overload
class C {
···public: // overloaded operators
& operator= (CC rhs) { //←copy-and-swap idiom
CCstd::swap(data_, rhs.data_);
std::swap(size_, rhs.size_);
return *this;
}
···};//class
Now, for a slight decrease in speed, you have less code, and exception-safety. No wonder it is so popular an idiom. Just remember, because of the pass-by-value parameter, the copy constructor is involved in this implementation — which some consider “impure”.
Function Objects
Sometimes we design classes, where the prime (or only) member function is one or more overloaded function call operators. We do this when we want to conceptually treat an object as a function. We call them “function objects” or “functors” for short. This makes such objects “callable”. The advantage of function objects over normal functions, is that each can carry separate state with it.
funcobjs.cpp
— Function Objects
/*!@file funcobjs.cpp
* @brief Function Objects Example
*/
#include <iostream>
class FO { //←‘function object’ type.
public:
(int state = 0) //←initialise state.
FO : state_{state} { }
void operator() () { //←overloaded function call.
std::cout
<< "FO::operator(): "
<< state_ << std::endl;
}
private:
int state_; //←‘functor’ instance state.
};//class
void cal (FO parm) { //←‘functor’ as parameter.
std::cout << "cal(); "; parm();
}
(int state) { //←returning a ‘functor’.
FO ret return FO{state};
}
int main () {
using std::cout; using std::endl;
(123); FO f2(456); //←create ‘functor’ objects.
FO f1
(); f2(); //←call ‘functors’.
f1(f1); cal(f2); //←pass ‘functors’.
cal
= ret(111); f2 = ret(222); //←assign returned ‘functors’.
f1 (); f2(); //←call new ‘functors’.
f1(FO{333}); cal(ret(444)); //←pass returned ‘functors’.
cal
return EXIT_SUCCESS;
}
As you can see, we can very easily manipulate a value of type
FO
(Function Object), as “just a value”, but abstractly, we
think of it as a function we are passing around, storing, returning and
calling. All courtesy of the ability to
overload the function call operator.
NOTE — Callable Expressions
Since templates, and in particular function templates, exand to text and are then compiled, the type of an expression on which the function call operator is applied, does not care about the exact type of the expression — only if it is callable.
For algorithms (function templates) in the C++ library, you can therefore pass:
- A pointer-to-function type expression.
- A function object (with overloaded function call operator).
- A lambda expression.
The choice is entirely up to the programmer, regarding which of the three to pass.
Overloaded Cast Operators
Sometimes, hopefully not often, we want to be able to cast from one
type A
, to another type, let's say B
. Maybe we
even want to cast the other way around. Given a class C
, to
create a cast operator that can convert it to double
, as
example, would require an overloaded cast operator:
overloading cast operator
class C {
public:
operator double() {
return static_cast<double>(member_);
}
private:
size_t member_;
};//class
···{};
C cdouble d(c); //←implicit cast operator.
double d = c; //←implicit cast operator.
= C{}; //←implicit cast operator.
d = static_cast<double>(c); //←explictly call cast operator. d
If you defined the cast operator with the explicit
specifier, the compiler will not implicitly call it. The only way to
call it then, is with static_cast<double>()
.
To convert a double
to the class C
above,
we must write a constructor that takes one
argument (or can be called with only one argument, to be precise). If,
like the overloaded cast operator, it was not marked
explicit
, the compiler will automatically call it on a
double
expression, when a C
type was expected.
Otherwise, if marked explicit
, you can call it directly, or
with static_cast<C>()
.
constructor with one parameter is a cast operator
class C {
public:
explicit C (double m) //←also a cast operator.
: member_{static_cast<size_t>(m)}
{ }
private:
size_t member_;
};//class
···{};
C cdouble d = 123.456;
= static_cast<C>(d); //←explictly call `C(double)`.
c = static_cast<C>(123); //←explictly call `C(double)`. (1)
c = static_cast<C>(1.23); //←explictly call `C(double)`. c
(1)
Because an implicit overload from int
to double
exists, the compiler will first convert the
int
to a double
and then call
C(double)
to perform the “conversion”.
IMPORTANT — Explicit Specifier
Note that if we did not use the explicit
specifier, none
of the static_cast
s in the example code above would have
been necessary. This can easily hide problems and errors, which is why
we recommend that you almost always specify any constructor, that can be
called with one argument, as explicit
.
Overloading Insertion & Extraction Operators
These are really the binary left-shift and right-shift operators respectively, but in the context of streams, we call them insertion, or extraction, operators. The C++ standard library implements these operators with a certain consistent pattern, which you should follow, if you want your overloaded versions to work like the library's.
These operators cannot be overloaded as member functions, because the
library's pattern expects a stream type as first parameter. If they were
member functions, the this
pointer would be the first
parameter in each. If these overloaded operators must access private
data, they must be declared as friend
functions inside the class.
overloaded insertion and extraction operators patterns
std::ostream& operator<< (std::ostream& lhs, const C& rhs) {
//(1)
···return lhs;
}
std::istream& operator>> (std::ostream& lsh, C& rhs) {
//(2)
···return lhs;
}
(1)
& (2)
You can do whatever necessary
where the ‘···
’ placeholder appears.
Further Rules
- New operators cannot be created.
- The precedence of operators cannot be changed.
- The number of operands cannot be changed.
- Normally commutative operators are not commutative when overloaded.
- Sequence guarantees are lost when overloading
&&
,||
and,
(comma) operators. - The scope resolution (
::
), member selection (.
), ptr-to-member select (.*
) and conditional (?:
) operators cannot be overloaded at all. - The indirection member selection operator (
->
) is restricted to either returning an expression for which->
is valid, a raw pointer, or an object whose type overloaded->
.
Full Circle
For a class as simple as Circle
, we need no more. We
present the complete versions here. We kept comments to a minimum in the
code, so that you can focus on the language syntax.
Circle.hpp
— Final Circle Specification
/*!@file Circle.hpp
* @brief Specification of class `Circle`
*
* **NOTE**: This `Circle` class is way too complex considering its
* design. It is a pedagogical example to illustrate syntax and is not
* necessary useful outside of that context, or without the accompanying
* discussion.
*/
#include <iostream>
#include <cmath>
#if !defined HF24AC6EF5444998AD0DFD0CF9D2DE2
#define HF24AC6EF5444998AD0DFD0CF9D2DE2
class Circle {
public: // symbolic constants
constexpr static double PI = 3.14159265359;
public: // ctors, destructor & assignment
() : Circle(1.0) { } //←ctor delegation
Circle(const Circle&) = default;
Circleexplicit inline Circle(double radius);
~Circle () { }
& operator= (const Circle& rhs) {
Circleif (this != &rhs)
radius_ = rhs.radius_;
return *this;
}
public: // accessor & utility methods.
auto radius() const -> double { return radius_; }
auto circum() const -> double { return 2.0 * PI * radius_; }
auto area() const -> double { return PI * radius_ * radius_; }
auto setRadius(double radius) -> void;
// optionally, set `radius_` via area or circumference:
auto setArea (double area) -> void {
(sqrt(area / PI));
setRadius}
auto setCircum (double circum) -> void {
(circum / (2.0 * PI));
setRadius}
operator double() { return radius_; }
bool operator< (const Circle& rhs) { return radius_ < rhs.radius_; }
private: // data member(s)
double radius_;
friend std::ostream& operator<< (std::ostream& os, const Circle& rhs);
};//class
inline Circle::Circle(double radius) {
if (radius <= 0.0)
throw std::runtime_error(
"Circle(double): Negative or 0 radius!");
radius_ = radius;
}
#endif
The definition of Circle(double)
could have been inside
the class, but it serves as an example of how to define
inline
functions outside the class.
The setArea()
and setCircum()
functions
deliberately call setRadius()
to avoid having to perform
validation on the argument passed — setRadius()
will throw
the exception, if necessary.
Circle.cpp
— Final Circle Implementation
/*!@file Circle.cpp
* @brief Implementation of class `Circle`
*/
#include "Circle.hpp"
#include <iostream>
auto Circle::setRadius (double radius) -> void {
if (radius <= 0.0)
throw std::runtime_error("setRadius(): Negative radius!");
radius_ = radius;
}
std::ostream& operator<< (std::ostream& os, const Circle& rhs) {
<< "R:" << rhs.radius_ << ", "
os << "C:" << rhs.circum() << ", "
<< "A:" << rhs.area() << std::endl;
return os;
}
The operator<<
(insertion operator) is a
friend
function, but did not have to be (it could
have called radius()
instead of using
radius_
). It exists just to provide an example pattern for
less trivial classes. We had to make it a “normal” function, since we
wanted to control the first parameter.
main.cpp
— Final Circle Client
/*!@file main.cpp
* @brief Circle Client/User Example Program
*/
#include "Circle.hpp"
#include <iostream>
int main () {
using std::cout; using std::cin; using std::endl;
double radius{};
<< "Radius?: ";
cout if (!(cin >> radius)) {
std::cerr << "Garbage input. Bailing." << endl;
return EXIT_FAILURE;
}
try{
(radius); //←might throw exception.
Circle c1<< c1 << endl; //←dump all attributes.
cout .setRadius(c1.radius() * 2.0);
c1
<< "c1's radius = " << c1.radius() << endl;
cout << "c1's circum = " << c1.circum() << endl;
cout << "c1's area = " << c1.area() << endl;
cout
.setArea(c1.area());
c1<< "c1's radius = " << c1.radius() << endl;
cout
double r = c1; //←implicit cast.
<< "r = " << r << endl;
cout {c1}; //←copy ctor.
Circle c2
if (!(c1 < c2) && !(c2 < c1)) //←equality in terms of
<< "c1 == c2\n"; // less-than.
cout }
catch(std::exception& ex) {
std::cerr << "Exception: " << ex.what() << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
The body of the main()
function has changed completely
from the previous examples, simply because we have “proper” constructors
and member functions to use.
NOTE — Alternative Circle Implementation Example
Another implementation of a Circle
class can be found in
a separate article: C++ Circle Code
Organisation, which also contrasts it with various ways a “circle
calculator” program in C++ can be organised, with or without
encapsulation.
Inheritance
A major component of object-oriented programming, is the ability to re-use code from existing classes via a concept called inheritance. This is syntactically very simple in C++, but to make practical use of it, the class hierarchy and responsibilities should be designed — especially considering that C++ supports multiple inheritance (can inherit from more than one base class).
Inheritance establishes a relationship between two classes. One is called the derived class, and the other the base class. Synonyms do exist: subclass and superclass (base), for example. Some even use “child class” and “parent class” (base)!
Syntax and Behaviour
Given an arbitrary, trivial base class, called Base
, we
can inherit a derived class from it, which we will trivially call
Deriv
:
class Base { ··· };
class Deriv: public Base { ··· };
That is it. Until you have good reason, use public
derivation as above. We use public
inheritance for the
“is-a” or “works-like-a” rule of thumb, so this means abstractly, that
“a Deriv
object is a
Base
” — like “a car is a vehicle”, or “a circle is a
shape”. On the other hand, when you encounter the observation that a
class is “implemented in terms of another”, you should use
private
inheritance (or membership / aggregation).
basederiv.cpp
— Trivial Inheritance
/*!@file basederiv.cpp
* @brief Trivial Inheritance Example
*/
#include <iostream>
namespace common {
using std::cout; using std::endl; using std::tuple;
using std::make_tuple; using std::tie; using std::get;
}
class Base { //---------------------- base class
public: // ctor(s) and [dtor]
explicit Base (int data = 0)
: bdata_(data) //←initialise `bdata_`.
{ } //←nothing to do here.
public: // ‘methods’
int bdata() const { //←arbitrary member function;
return bdata_; // (accessor).
}
private: // data member(s)
int bdata_;
};//class
class Deriv //----------------------- derived class
: public Base { //←public inheritance.
public: // ctor(s) and [dtor]
()
Deriv : Deriv(0, 0.0) //←delegate to other ctor.
{ }
(int b, double d)
Deriv : Base(b) //←call specific `Base` ctor. (1)
, ddata_(d) //←initialise `ddata_`.
{ } //←nothing to do here.
public: // ‘methods’
double ddata () const { //←arbitrary member function;
return ddata_; // (accessor).
}
private: // data member(s)
double ddata_;
};//class
int main () { //--------------------- program / client
using namespace common;
(111);
Base b(222, 3.33);
Deriv d
<< "b::bdata = " << b.bdata() << endl;
cout << "d::Base::bdata = " << d.bdata() << endl;
cout << "d::Deriv::ddata = " << d.ddata() << endl;
cout
return EXIT_SUCCESS;
}
(1)
Normally, the default Base
class constructor would be called before the code of the
Deriv
ed class constructor executes. The constructor member
initialiser list syntax allows you to override which base class
constructor is called.
The code in main()
shows that a Deriv
object has a bdata()
member, which can be accessed
as a public method, as if it was defined in Deriv
. If we
used protected
or private
inheritance, it
would not have been accessible in main()
. The code also
shows that d
, a Deriv
type object, did in fact
inherit the bdata_
member, but it is not accessible by name
inside Deriv
code, nor in main()
— both cases
will require calling the public bdata()
function to provide
(read-only) access.
Hiding Inherited Members
Since a base class is effectively in a higher-level scope than the scope of a derived class, a member in a derived class can hide a member in the base class with the same name. This is seldom done deliberately, but since it is not an error, you will have to guard against inadvertently hiding members. If you do, however, the hidden members can still be accessed with qualified lookup (scope resolution operator).
hiding, not overriding, nor overloading, inherited member function
class Base {
···public: // member to be hidden
void member () { std::cout << "Base::member()\n"; }
···};//class
class Deriv: public Base {
···public: // hiding inherited member
void member () {
::member(); //←qualified lookup.
Basestd::cout << "Deriv::member()\n";
}
···};//class
int main () {
···(11); Deriv d{22, 33.33};
Base b
.member(); //←call `Base::member()`.
b.member(); //←call `Deriv::member()`.
d.Base::member(); //←call `Base::member()`.
d
return EXIT_SUCCESS;
}
The Base::member()
can still be accessed by being
specific about its scope, either inside the derived class, or in client
code (as long as it has public
access, of course).
Overloading Inherited Members — Or Not
One should be particularly careful about overloading inherited member functions — it looks like overloading, except it is not. It will still hide the member and overload resolution will not find the best overload during resolution. In fact, it will not find any overloads from the base class.
hiding, not overloading, inherited methods
class Base {
···public: // member to be hidden
void member (long parm) {
std::cout << "Base::member(long)" << parm << std::endl;
}
void member () { //←overloading.
std::cout
<< "Base::member()\n";
<< std::endl;
}
···};//class
class Deriv: public Base {
···public: // hiding inherited member
void member (double parm) { //←overloading & hiding.
std::cout
<< "Deriv::member(double)\n";
<< parm << std::endl;
}
···};//class
int main () {
···(111); Deriv d(222, 3.33);
Base b
.member(123L); //←calls `Base::member(long)`.
b.member(123L); //←calls `Deriv::member(double)`.
d.Base::member(123L); //←calls `Base::member(long)`.
d.Base::member(); //←calls `Base::member()`.
d#if defined SHOW_ERRORS
.member(); //←ERROR. cannot find member.
d#endif
···}
When calling d.member(123L)
, the compiler does not find
the best overload in Base
. This is because
overloading looks in the current scope for an overload, not in
higher scopes. We can still force the scope with the scope resolution
operator, as shown. It even affects the clear winner and only
option for the overload that takes no parameters — it still does not
find it by itself.
This means that, for all practical purposes, it is not possible to overload inherited member functions in a useable way.
Inheriting Constructors & Access Control
These two topics are unrelated from an abstraction perspective, but their syntax is similar, so we use the same example to illustrate both features.
The using
declarator can be used in a derived class, for
two reasons, apart from type aliasing.
Firstly, it can be used to inherit constructors. The syntax
is simple:
‘using
‹base›::
‹base›;
’.
This will make all ‹base› constructors usable as
‹deriv› constructors; all except the a) default constructors,
b) the copy constructor, and c) the move constructor — none of these are
inherited.
Secondly, it can be used to change the inherited access of
one or more members. This is independent of the type of inheritance used
(‘···public
‹base›’,
‘···protected
‹base›’,
‘···public
‹base›’).
usingderiv.cpp
— Using Example
/*!@file usingderiv.cpp
* @brief Using in Derived Classes Example
*/
#include <iostream>
namespace { using std::cout; using std::cin; using std::endl; }
class B {
public: B (int b = 0) : bdata_(b) { }
protected: int data () const { return bdata_; }
private: int bdata_;
};//class
class D : private B { //←could also have been `public`
// or `protected` here.
public:
using B::B; //←inherit `B` ctors.
using B::data; //←force `public:`.
};//class
int main () {
(111); //←calls `B::B(int)`.
B b(222); //←calls `B::B(int)` (inherited).
D d
<< d.data() << endl; //←allowed.
cout // b.data() //←not allowed.
return EXIT_SUCCESS;
}
The B::data()
member was inherited by D
,
and made public
with using
. This is why it can
be called on a D
object. It would still not be possible to
call it via a B
object. If B::data()
was
private
in the class B
, then this would not
have worked — private
members are never accessible
by any code, unless declared as friend
s.
You could also use this syntax to reduce the access. For
example, if the base class Base
has a public
function F()
, and the derived class Deriv
inherits as public
from Base
, it can limit
access to clients with: ‘using Base::F();
’. The access
control in effect, when you use that statement, determines its new
access level.
Polymorphism
If you were to investigate the assembler-generated machine code for a
function call, you will notice that it will be something like:
CALL 12345h
(call some address). This is called
“static binding”, which means it is resolved at compile time (often by
the linker). The following code uses a table of pointer to member
functions to dynamically (at runtime) resolve calls to
functions. In other words, the address is selected from an array, and
then called:
ptrtomemfuncs.cpp
— Pointer to Member Function
/*!@file ptrtomemfuncs.cpp
* @brief Pointer to Member Functions Example
*
* This is an example of resolving function calls at runtime, albeit
* slightly clumsy. We'll use C++'s built-in features later.
*/
#include <iostream>
namespace common { using std::cout; using std::endl; }
class Base {
private:
static void (Base::*vt_[2])();
protected:
void (Base::**vp_)();
public:
() : vp_(vt_) { }
Base public:
void vfunc1 () { std::cout << "Base::vfunc1()\n"; }
void vfunc2 () { std::cout << "Base::vfunc2()\n"; }
void vcall (int n) { (this->*vp_[n])(); }
};//class
class Deriv
: public Base {
private:
static void (Base::*vt_[2])();
public:
() { vp_ = reinterpret_cast<void (Base::**)()>(vt_); }
Derivvoid vfunc2 () { std::cout << "Deriv::vfunc2()\n"; }
};//class
void (Base::*Base::vt_[2])() { &Base::vfunc1, &Base::vfunc2 };
void (Base::*Deriv::vt_[2])() {
&Base::vfunc1,
reinterpret_cast<void(Base::*)()>(
&Deriv::vfunc2 // ‘override’ vfunc2.
)
};
int main () {
using namespace common;
{}; Deriv d{};
Base b
.vcall(0); b.vcall(1); //←calls `Base::vfunc1&2()`.
b.vcall(0); //←calls `Base::vfunc1()`.
d.vcall(1); //←calls `Deriv::vfunc2()`.
d
* p = &b;
Base->vcall(0); p->vcall(1); //←calls `Base::vfunc1&2()`.
p= &d;
p ->vcall(0); //←calls `Base::vfunc1()`.
p->vcall(1); //←calls `Deriv::vfunc2()`.
p
return EXIT_SUCCESS;
}
The very last call is significant: if we did not look up the
“overridden” function (Deriv::vfunc2
), it would not have
been called; instead, because of the type, Base::vfunc2
would have been called.
Every class above has its own copy of the vt_
member.
Consequently, a derived class can either initialise it with the address
of an inherited function, or can override one with the address of its
own function. The calls become lookups at runtime, which find a
function's address, and then call it. This is not how real
function calls work, as we have seen. This particular implementation is
very clumsy, but the idea is very desirable.
Virtual Functions
C++ can be instructed to create something similar, without all the clumsiness. It can create a single static “vtable” (virtual function table) for each class, and can create a “vptr” instance data member automatically for each object. The “vptr” for each object would point to the “vtable” for its own class.
The compiler can further initialise the “vtable” automatically, with
the addresses of functions marked to appear in the table. The keyword
controlling this is virtual
, which leads to “virtual
functions” as a term. You do not have to do anything else: just
declaring one function as virtual
, will cause it
to create the “vtable” and the “vptr” member.
At the point of call to a virtual function, it will work like our
vcall()
above: it will lookup up by offset the
function to call. Since derived classes inherit base members, they will
inherit the same list of function pointers, but they can optionally
override one or more. A call to “function number
2
” will then find the overridden function, regardless of
the type.
virtfuncs.cpp
— Virtual Functions
/*!@file virtfuncs.cpp
* @brief Virtual Functions Example
*/
#include <iostream>
namespace common { using std::cout; using std::endl; }
class Base {
public:
virtual void vfunc1 () { std::cout << "Base::vfunc1()\n"; }
virtual void vfunc2 () { std::cout << "Base::vfunc2()\n"; }
};//class
class Deriv
: public Base {
public:
void vfunc2 () override { //←keyword `override` is
std::cout // optional, but recommended.
<< "Deriv::vfunc2()\n";
}
};//class
int main () {
using namespace common;
{}; Deriv d{};
Base b
.vfunc1(); b.vfunc2(); //←calls `Base::vfunc1&2()`.
b
.vfunc1(); //←calls `Base::vfunc1()`.
d.vfunc2(); //←calls `Deriv::vfunc2()`.
d
* p{&b};
Base->vfunc1(); p->vfunc2(); //←calls `Base::vfunc1&2()`.
p= &d;
p ->vfunc1(); //←calls `Base::vfunc1()`.
p->vfunc2(1); //←calls `Deriv::vfunc2()`.
p
return EXIT_SUCCESS;
}
This is certainly much simpler, and absolutely painless. It is again
important to notice that p
has type Base*
, but
the two places we write: p->vfunc2()
, it calls
different functions. The term “polymorphism”
refers to this whole concept, syntax and process.
Classes that derive from Deriv
, will inherit
Deriv
's overrides, but still have the option to override
any or all of the inherited functions, whether they were overridden by
their direct base class, or not. Instead of override
, you
can use final
, in which case it would
not be possible for derived classes to
override the function any more.
IMPORTANT — Virtual Destructors
It is crucial that you remember to make your destructor
virtual
in any class that has at least one
virtual
function. If you forget this, situations will arise
when your object will only be half-destructed (only a base class
destructor will be called).
When unsure about which sort of inheritance to use, another rule of
thumb is that, when you feel the need to implement virtual
functions, you should probably use public
inheritance. But
never use public
inheritance simply for code re-use.
Abstract Classes
In an object-oriented design, we sometimes recognise that, to create
a useful family of classes, we can identify a class whose only purpose
would be to provide structure to derived classes; there would be no need
for an object of this class. In C++, we can create such
classes, by simply creating at least one pure virtual function.
This is a virtual
function with ‘= 0;
’ instead
of a function body.
Such a class is now automatically an abstract class, and thus will have these desirable effects:
- No object of the abstract class can be created.
- Derived classes will have to override the pure virtual function(s).
- Pointers and references of the class can still be created as variables, members, or parameters.
Only classes directly deriving from an abstract class, have to override the pure virtual functions.
abstract class contains at least on pure virtual function
class Abstract {
public:
virtual void abs_func () = 0; //← “abstract function”. (1)
};//class
class Deriv: public Abstract {
public:
void abs_func () override { } //← “must override”.
};//class
···{};
Deriv d* p{&d}; p->abs_func();
Abstract& r{ d}; r. abs_func();
Abstract ···
(1)
The virtual
is optional on pure virtual
functions syntax, but good practice.
The most common classic example, is where we have an abstract
Shape
class. Here, we want all “shapes” to be able to
“draw” themselves; but the logic for each “shape” is different. Doing
something like the following, is cumbersome, inconvenient, and
unmaintainable:
inappropriately dealing with different draw() implementations
enum class ShapeKind { Circle, Rectangle, Triangle };
···switch (obj->type) {
case ShapeKind::Circle:
static_cast<Circle*>(obj)->draw(); break;
case ShapeKind::Rectangle:
static_cast<Circle*>(obj)->draw(); break;
···}
Since the kind of code above is simply not acceptable, we can create
an abstract Shape
class, with a pure virtual function
called draw
, which all derived classes must implement.
polymorphism and virtual functions illustration
class Shape { //←abstract class…
public: virtual void draw () = 0; //←because of this.
};//class
class Circle : public Shape { //←`Circle` “is-a” `Shape`.
public:
void draw () override { ··· } //←`Circle` rendering
};//class
class Rectangle : public Shape { //←`Rectangle` “is-a” `Shape`.
public:
void draw () override { ··· } //←`Rectangle` rendering.
};//class
class Triangle : public Shape { //←`Triangle` “is-a” `Shape`.
public:
void draw () override { ··· } //←`Triangle` rendering.
};//class
···* dwg[]{ // randomly, dynamically, add “shapes”.
Shapenew Circle{}, new Rectangle{}, new Triangle{},
new Circle{}, new Triangle{}, new Rectangle{}
};
for (size_t i = 0
; i < sizeof(dwg)/sizeof(*dwg)
; ++i) {
[i]->draw(); //←call one of three `draw`s.
dwg}
//delete memory ···
Forgive the C-style array, and raw new
, but it is so we
can emphasise that one statement, containing the call
dwg[i]->draw()
, will call different
implementations of the function, depending on the runtime type
of the pointer, even though the compile-time type of dwg[i]
is Shape*
. That is classic polymorphism; we just added the
concept of an abstract class on top.
Summary
Classes are a crucial tool to create manageable code, and to create abstractions that remove clients from implementation detail. At minimum, we should use encapsulation, which is the main point here. C++ offers us the following encapsulation features:
Syntax — Both the
class
andstruct
keywords create user-defined types. They really create two types (e.g. ‘struct X…
’ createsstruct X
andX
types), and they are synonyms. The only real difference betweenstruct
andclass
is the default access control, which isprivate
in aclass
, andpublic
in astruct
. However, usestruct
only for “plain old data” (like in C). Aclass
can be forward declared, but its use is limited to being used as a reference or pointer only.Access Control — We can design the level of access to members that various parts of programs can have with
private
,protected
andpublic
. Classes may have inaccessible members, which they can never access, and that can only be obtained via the inheritance ofprivate
members of a base class.Initialisation — Customised initialisation of objects can be performed with constructors. They only ever work with new uninitialised objects. Base class constructors are called before the derived class constructors.
Special Members — The C++ compiler will, under varying conditions, provide us with all, or some of: a) default constructor, b) destructor, c) copy constructor, d) copy assignment operator, e) move constructor and f) move assignment operator. We can indicate whether we want the default versions (
=default
), or if some of them should be made inaccessible (=delete
).Specific Constructors — Objects can be initialised with custom overloaded functions with a special syntax, which is what makes them constructors. We should carefully consider and design constructors. Constructors can be called explicitly, in which case they create temporaries.
Constructor Member Initialiser List — This is a special syntax only applicable to constructors consisting of a colon (‘
:
’) between the constructor header and its body. We can construct (initialise) data members here, before any code in the body executes. This is an efficiency consideration.Constructor Delegation — An extension to the member initialiser list syntax, constructors can delegate to (call) other constructors. This means validation and responsibility can be concentrated in one part of the class, and not spread across many constructors.
Base Class Construction — Another way the constructor member initialiser list can be used, is to designate a specific base class constructor to call, instead of the default constructor.
Move Semantics — Together with rvalue references, and their significance in overloading (they participate in the signature), the compiler can distinguish between functions with the same name, but which are called with an lvalue argument, or an rvalue argument. This means it can call a move constructor, or a move assignment operator, instead of the normal “copy” versions, which significantly enhances performance (it adds no other value).
Destructor — When constructors acquire resources (e.g., memory), we can release those resources in the destructor. This is one feature of C++ which makes it very different from garbage-collecting languages like Java and C# — unlike those, C++ has deterministic destruction. In classes with virtual functions, the destructor should also be virtual. Base class destructors are automatically called after the derived class' destructor.
Symbolic Constants — Although not as abstract as it could have been, we can create symbolic constants with
constexpr
, as long as we remember to also make itstatic
. Older code may “fake” symbolic constants withenum
values.Instance Data Members — By design, we can choose which data members every object must possess. Since objects are formally called “instances of a class”, you may understand why we talk about “instance members”. They constitute the state of an object.
In-Class Initialisation — We can choose to initialise instance data members as if they are normal variables (syntactically). This creates a special situation where the compiler will automatically perform this initialisation before any constructor is called — but if the constructor explicitly initialised the member, this special in-class initialisation will not be performed.
Shared Data Members — Data members can be defined as
static
, which means there will only ever be one copy, effectively forcing objects to “share” the same member. It can be accessed via an object as if an instance member, but preferably should be accessed via the class name and the scope resolution operator.Instance Member Functions/Methods — Logically, every object will get copies of these functions. Practically, it won't, since that would be monumentally inefficient. To achieve the abstract model of code instances, every instance member function automatically gets a first parameter called
this
, which has typeC*
, whereC
is the class to which the function belongs. The argument for this parameter is also automatically passed when the object is used to invoke the function.Shared Member Functions — Members can also be defined as
static
, which means they do not logically belong to any one object. Practically, this mean they do not get athis
pointer parameter. They can be accessed via an object as if instance members, but preferably should be accessed via the class name and the scope resolution operator. The compiler will also implicitly prefix ‘this->
’ in front of all member access expressions.Inline Member Functions — Member functions can be defined
inline
in the class by simply adding the body. Or they can be declaredinline
in the class, and definedinline
outside the class (in the same file).Constexpr Member Functions — Member functions can be defined
constexpr
in the class. This also applies to constructors. It is not common to create all the functions and data members as constant expressions withconstexpr
, but if you do, we call it a “constexpr
class”. It has very limited use, but you can find a use if you look for it (“…if you have a hammer…”).Inheritance Syntax — The default inheritance is
private
, but you should explicitly specify either:public
,private
orprotected
, depending on your design. Which access specifier you use, controls the access with which the members in the base class will be inherited in the derived class. Members that areprivate
in the base class, will always be inaccessible in the derived class.Pointer & Reference Conversions — In an inheritance relationship, a base class pointer or reference can point to, or refer to, a derived class object without an explicit cast. The reverse is not true, but can still be accomplished with
static_cast<>
(explicit cast).Class Scope — Apart from the fact that a class acts like a namespace, we must understand that a derived class is in a nested scope with respect to its base class(es). This is where the scope resolution operator can come in handy to explicitly qualify access, when members hide others.
Polymorphism — An important object-oriented programming concept, this allows the same call to potentially resolve in calls to different functions. In C++, we implement this with functions declared as
virtual
.Virtual Functions — Functions declared
virtual
in a base class, can be optionally overridden in a derived class. Overridden functions are automatically resolved at runtime. Derived classes can override a virtual function, but prevent further overriding by its own derived classes with thefinal
keyword, instead ofoverride
.Abstract Classes — Another import OOP concept, this allows us, by design, to create classes for the express purpose of serving as a base class for derivation. A class is abstract when it declares at least one “pure virtual function” — which must be overridden by any directly derived class.
2023-08-04: Add virtual to pure virtual function in Shape
class.[brx]
2019-03-28: Change curly brace initialisation to parentheses.[brx]
2019-02-21: Basic editing; style convention; note about
callables.[brx]
2018-09-13: Corrections. (s/date/data/). Few clarifications. Label all
code extracts. [brx]
2018-09-13: Corrections. (s/date/data/). Few clarifications. Label all
code extracts. [brx]
2018-05-17: Added fact that linker address resolving is also logically
“at compile time”. [brx]
2017-11-30: Added note controlling base class constructors from derived
classes. [brx]
2017-11-30: Thanks to Brian Khuele, added pure virtual function
clarification. [brx]
2017-11-17: Update to new admontions and definitions styles.[brx]
2017-10-08: Added inheritance. Review & edit. [brx;jjc]
2017-10-07: Reorganisation. Addition of several topics, excluding
inheritance. [brx]
2017-09-12: Created. [brx;jjc]