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 us­ing classes, an un­der­stand­ing of the es­sen­tial language components and syntax specific to C++ is required. This constitutes the minimum syntactical knowledge re­quir­ed 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 of const;
  • 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 ‘structclass MyType’, it also creates ‘MyType’. Syntax-wise, they are equivalent, except that the de­fault access in a struct is public, while the de­fault 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-pro­gram­ming head­er, 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 (Uni­fied Mod­el­ling Language). The first aspect of OOP (Object-Oriented Programming) involves en­cap­su­la­tion, which, even used by itself, is useful. As an example: Visual Basic, up to Version 6, only sup­port­ed encapsulation — no inheritance or poly­mor­phism; 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 im­ple­ment an abstraction that encapsulates the state and behaviour of the object created from this mod­el. In C++, the term ‘object’ really just means ‘variable’ but not necessarily one with a name. We use ‘object’ when we want to em­pha­si­se that it is a value of a class type. Formally, in OO terminology, object means “in­stance of a class”. It is like saying a house is an in­stan­ce 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.

IMPORTANTRelated File Inclusion

Whenever this pairing of header and source files exists, whether it in­vol­ves clas­ses 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 mis­match 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 in­vol­ves permissions. For encapsulation, we only need to choose between two possibilities private and protected. For inheritance, we also could use protected:

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 sim­ple, 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: mem­bers 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 in­her­it­ed, like all other members, but because of access control, they are not accessible by code in the de­rived class. Again, the mechanism of inheritance, and the concepts of access control, in­vol­ve dif­fer­ent semantics — if a class cannot access members, it does not mean they were not in­he­rit­ed.

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 re­so­lu­tion op­e­ra­tor (qual­i­fied 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 si­mul­ta­ne­ous­ly 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 re­so­lu­tion op­e­ra­tor: ‘‹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`.
      Base::member();                //←qualified lookup of `member`.
      if (false) {
         this->member();             //←*recursive* (`Deriv::member`).
         member();                   //←*recursive* (`Deriv::member`).
         }
      }
};//class

int main () {
   using namespace common;

   Deriv dob{};
   dob.member();                     //←calls `Deriv::member()`.
   dob.Base::member();               //←calls `Base::member()`.
   dob.Deriv::member();              //←calls `Deriv::member()`.

   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 con­cept. 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), of­ten inferred from their names. If a member, for example, is called radius_ and hopefully means the radius of a circle, then we un­der­stand 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 of­ten re­fer­red to as instance data members. Other terminology that is common, is to refer to data mem­bers as “fields” — for some it means only instance members, and for some it seems to mean any data mem­ber, 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 be­cause we do not want code outside the class to maliciously, or accidently put in­val­id val­ues in data mem­bers, they are commonly defined with private: access. This could be the be­gin­ning of an ex­am­ple Circle class:

Circle.hppCircle 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 cor­res­pon­ding Circle.cpp file as well. This will ensure that we do not for­get later to #include the header file in the .cpp file.

Circle.cppCircle 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.cppCircle 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;

   Circle c1{};                      //←default ctor supplied implicitly.
   Circle c2(c1);                    //←copy ctor supplied implicitly.
   c1 = c2;                          //←assignment operator supplied implicitly.

   cout << "Pi = " << Circle::Pi << endl;

   #if defined SHOW_ERRORS
   cout << "Pi = " << c1.Pi << endl; //←not legal for `constexpr static` members.
   cout << "Radius = " << c1.radius_ << endl; //←`main` not allowed *access*.
   #endif

   return EXIT_SUCCESS;
   }

NOTEStatic 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 con­ven­tion, we avoid accessing mem­bers 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 ob­jects, unlike the other data members, where every object gets its own instance. Such static mem­bers must be defined in the implementation file (.cpp), but without static. They can be de­clar­ed 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.hppTrivial Class Specification 1
/*!@file  myClass.hpp
*  @brief Trivial Example Class *Specification*.
*/
class myClass {
private:
   static int count_;                //←*declare* `static` member.
public:
   myClass()   { ++count_; }         //←*define* ctor inline.
   ~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.cppTrivial 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 va­ri­ab­les de­clar­ed 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 ne­ver good. If not explicitly initialised, they are guaranteed to be zero-initialised if they are in­trin­sic types, or de­fault-initialised if a user-defined type. They will be initialised before execution enters main().

main.cppTrivial 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;

   myClass c1{}, c2{};

   cout
      << c1.count() << ' '           //⇒ 2
      << c2.count() << ' '           //⇒ 2
      << '\n'; 
   /* create a new scope
   */ {
      myClass c2{};                  //←*hide*/*shadow* higher `c2`.
      myClass c3{};                  //←another object `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 com­plete and robust im­ple­men­ta­tion, we would have to increment the count_ in the other con­struc­tors and as­sign­ment 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 de­fin­ed 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 un­clear 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 as­sem­bler level), they are implemented exactly like normal instance data members, but the com­pi­ler con­stant­ly checks that your code does not modify them. So “const member” really means “read-only mem­ber”, 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 lo­gi­cal­ly con­stant, 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 mem­ber func­tion”.

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 in­line func­tion'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 func­tion). To be exact: it does not have to be declared with an explicit inline inside the class, it is just good coding con­vention — a reminder to readers that it will be inline defined later.

Circle.hppCircle 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 de­fini­tion in the cor­res­pon­ding Circle.cpp file:

Circle.cppCircle 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: con­trol­ling access to private data in a class. It does not mean we blind­ly provide accessor methods for each and every data member. It is part of the ma­ny de­sign de­ci­sions you have to make. Having too many accessor methods, generally results in a loss of ab­strac­tion, 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.cppCircle 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;

   Circle c1{};                      //←default ctor supplied implicitly.
   Circle c2(c1);                    //←copy ctor supplied implicitly.

   c2.setRadius(12.34);
   c1 = c2;                          //←assignment operator supplied implicitly.

   cout << "c1 radius = " << c1.radius() << endl;
   cout << "Pi = " << Circle::Pi << endl;
   double area = Circle::Pi * c1.radius() * c1.radius();
   cout << "c1 area = " << area << endl;

   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 () {
   ···
   cout << "c1 area = " << c1.area() << endl;
   cout << "c1 circum = " << c1.circum() << endl;
   ···

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:

DefinitionThe ‘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 func­tions are cal­led on ob­jects 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 au­to­ma­ti­cal­ly pre­fix­es ‘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:

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 sig­na­tu­re of the function. The following two member functions are consider to have different sig­na­tur­es. The com­pi­ler will automatically match the non-const version, when selected on a non-const object, and the const version, if se­lec­ted 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 c{};
   const C& r{c};
   const C* p{&c};
   c.MF()  // calls `C::MF()`.
   r.MF()  // calls `C::MF() const`.
   p->MF() // calls `C::MF() const`.

This feature is especially useful when we write functions that return references to members or ele­men­ts, like the overloaded subscript operator. Otherwise, we would not be able to use some­thing like sub­script on a const object.

NOTEThe “noexcept” Specifier

The noexcept specifier after a function header, is independent of a const specifier be­ing 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 mem­ber func­tions”. Not only that, but they can be implicitly de­fined 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 im­ple­men­ta­tion file (.cpp file). The de­fi­ni­tion, as with the variable count_ below, must not be de­fined 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 mem­ber function. This is why they are sometimes referred to as in­stan­ce mem­ber functions (or methods, if you like). A member func­tion is declared static, or de­fin­ed static (im­pli­cit­ly inline), or defined inline static. If the def­i­ni­tion must go in the imp­le­men­ta­tion .cpp file, the static storage class specifier must be omitted in the def­i­ni­tion.

C.hppTrivial 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

NOTEOptional “inline”

Explicitly using inline for the static function SF1() is optional, just like with in­stan­ce mem­ber functions — if the body of a function is in the class, i.e., it is defined in the class, it is au­to­ma­ti­cal­ly 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.cppTrivial 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 mem­bers, without any object of type C being present in the expression.

main.cppTrivial Class Client
/*!@file  main.cpp
*  @brief Trivial Class C User/Client
*/
#include "C.hpp"
#include <iostream>

int main () {
   using std::cout;  using std::endl;

   cout // use scope resolution to call functions.
      << C::SF1() << ", " << C::SF2() << ", " << C::SF3()
      << endl;
   C ob{};
   cout // use member selection on object to call functions.
      << 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 ei­ther via the class name and scope re­so­lu­tion 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.hppTrivial Class Specification 2
/*!@file  myClass.hpp
*  @brief Trivial Example Class *Specification*.
*/
class myClass {
private:
   static int count_;                //←*declare* `static` member.
public:
   myClass()   { ++count_; }         //←*define* ctor inline.
   ~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.cppTrivial 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.cppTrivial 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;

   myClass c1{}, c2{};

   cout
      << c1.count() << ' '           //⇒ 2
      << c2.count() << ' '           //⇒ 2
      << myClass::count()            //⇒ 2    ←added
      << '\n'; 
   /* create a new scope
   */ {
      myClass c2{};                  //←*hide* higher `c2`.
      myClass c3{};                  //←another object `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 con­trol the first pa­ra­met­er, 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 op­era­tor 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 de­clar­ing 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 cer­tain circumstances. These special members always require special consideration — they can­not 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 fol­low­ing patterns are related to the special members of a class C.

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 spe­cial mem­bers 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: ctorconstructor, dtordestructor, dftdefault, decldeclared and asgnassignment operator.

User-declared vs Compiler-provided
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 con­struc­tor, co­py con­struc­tor, de­struc­tor, and over­load­ed copy as­sign­ment operator, even if you declare them with ‘=default’. Ideally, also declare the other two: move con­struc­tor and over­load­ed move as­sign­ment 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 mem­ber ‘=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:
   T data_;
public:
   Deriv (Deriv&& rhs)
      : 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 re­sour­ces, 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 as­sign­ment op­e­rat­or:

synthesized move assignment operator pseudocode
class Deriv
   : public Base {
private:
   T data_;
public:
   Deriv& operator= (Deriv&& rhs) {
      Base::operator= (static_cast<Base&&>(rhs));
      data_ = 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 re­sour­ces 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 re­turn 0.

Never write special members in terms of one another. Move members are only about per­for­man­ce. For example, the popular swap idiom can be used to provide exception safety for an over­load­ed as­sign­ment 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 in­it­ial­isa­tion syntax involves the assignment character, that is not the assignment operator — it is still in­it­ial­isa­tion, 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 de­ter­min­es the behavioural mechanisms employed by the compiler. Do remember though, that the con­text of a syn­tax, may change its semantics and thus initialisation behaviour (e.g. the same syn­tax is used to de­fine variables on the external level, on the internal level, and inside class or struct, but the in­it­ial­isa­tion rules, scopes and lifetimes are different).

Kinds of Initialisation

Here we discuss three possibilities, all formally defined, that concern initialisation when no spe­ci­fic val­ue is given. This therefore does not involve specific constructors, or the copy con­struc­tor, but the default constructor may come into play, depending on the ‹type› of the ‹obj› (variable):

initialisation examples and comments
// external level:
//
T EV;           //←zero-initialised → default-initialised.
static T ES;    //←zero-initialised → default-initialised.

// internal level:
   {
   T X();                          //←function declaration.
   T I;                            //←default-initialised.
   T J{};                          //←value-initialised.
   T K{T()};  T L2{T{}};           //←value-initialised.

   T* P;
      P = new T;                   //←default-initialised.
      P = new T();                 //←value-initialised.
      P = new T{};                 //←value-initialised.
   }
struct S1 {
   T m_;  S1() : m_() { }          //←`m_` value-initialised.
   };
struct S2 {
   T m_;  S2() : m_{} { }          //←`m_` value-initialised.
   };
struct S3 {
   T m_;  S3()        { }          //←`m_` default-initialised.
   };

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.

GUIDELINEDeprecated 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 im­pli­cit con­struc­tors 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:

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 com­pil­er-sup­plied ver­sions. If you pro­vide 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
   Circle() = default;
   Circle(const Circle&) = default;
   Circle(Circle&&) = default;
   Circle& operator= (const Circle&) = default;
   Circle& operator= (Circle&&) = default;

   Circle(double radius)
      : radius_{radius} { }
   // ↑_______________↑ ctor member initialiser list 
};//class

The C++11 ‘… = default;’ statements above simply state, explicitly, that we are happy with the de­fault 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 mem­ber ini­tia­li­ser lists”, which consists of a : between the header and the body, followed by a list of data mem­ber names, each with a parentheses-delimited, or a brace-delimited, initialiser. Now we can ex­pli­cit­ly ini­tia­lise a new Circle variable with a radius, if we want:

calling various Circle constructors
int main () {
   Circle c1(1.2);       //← or: `Circle c1(1.2);`, or `Circle c1 = 1.2;`
   double radius = 1.2;
   Circle c2(radius);    //← any `double` will do, not just a literal.
   c1 = 12.34;           //← same as: `c1 = Circle(12.34);` or:
//  `c1 = static_cast<Circle>(12.34);` 

Constructors can be called explicitly. This will create a temporary object, which can be copied, as­signed, or returned. Constructors can also delegate (indirectly call) another constructor to per­form some, or all, of the initialisation work. For example, we could modify the Circle's de­fault con­struc­tor, to delegate to the constructor that takes a double as parameter:

constructor delegation
class Circle {
   ···
public: // ···
   Circle () : Circle(1.0) { }
   ···
};//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 cir­cum­sta­nces (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).

TerminologyA ‘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:

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 re­turn large containers by value from functions, due to the magic of move semantics. If the com­pil­er 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 ef­fec­tive­ly force the com­piler to use the move constructor or move assignment operator.

IMPORTANTImplicit Cast Constructor

Any constructor, that takes one argument of a different type than the class, is im­pli­cit­ly 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 re­sour­ce ac­quisi­tion is initialisation (RAII) idiom, which can be used in languages with de­ter­mi­nis­tic de­struc­tion. 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 de­struc­tor, 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 deleteing 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 ob­ject that goes out of scope — the very moment it goes out of scope. We call this deter­min­istic des­truc­tion. 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 re­sult 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 de­fine 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 in­i­tia­li­sa­tion on all members before a constructor is called, but only as a consequence of a con­struc­tor call. If the constructor in question explicitly initialises a member that has been defined with in-class in­i­tia­li­sa­tion, this initialisation will not be performed.

The initialisation expression is a runtime expression, which means it can even be a function call.

icinit.cppIn-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:
   C () { }                        // `d1_←?`, `d2_←123`, `d3_←4.56`.
   C (int d2)
      : d1_{}, d2_(d2)             // `d1_←0`, `d2_←d2`,  `d3_←4.56`.
      { }
   C (double d3)
      : 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 c1;  C c2{};  C c3(111);  C c4(222.333);
   # line 1
   L c1.dump();                   //←`d1_` has undefined value.
   L c2.dump();                   //←`d1_` has undefined value.
   L c3.dump();                   //←`d1_` default-initialised.
   L c4.dump();

   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:

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:

  1. use member initialiser list entry, if it exists, or
  2. use an in-class initialiser, if it exists, or
  3. 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 with­out any constructor: C c1; versus: C c2{};.

IMPORTANTIn-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 al­ways 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 clas­ses. 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, fol­low­ed by the operator symbol. Whitespace between the operator keyword and the symbol is not syntactically significant; nor is whitespace after the symbol.

   A + B  ≡ operator+(A,B)   ‖  A + B ≡ A.operator+(B)
   A * B  ≡ operator*(A,B)   ‖  A * B ≡ A.operator*(B)
   A << B ≡ operator<<(A,B)

Since the last example is normally used as the insertion operator, we did not show its alter­native op­tion as a member function, since it would not have been possible to get a stream as the first par­am­eter, 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 convert­ing it to function calls, should illuminate the benefits of operators:

   A + B * C - D ≡ operator-(operator+(A, operator*(B, C)), D)

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 over­loading — entirely based on signatures. It is made possible because of an in­de­pen­dent 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. White­space between and the operator keyword, or the left parenthesis, is allowed, and not sig­ni­fi­cant and does not contribute to the semantics.

The …operator new… actually has many variations; only the basic versions are shown. The same app­lies to …operator delete….

NOTECast 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), * (multi­plica­tion and in­dir­ec­tion), /, %, ^, &, |, ~, !, =, <, >, <=, >=, ==, !=, +=, -=, *=, /=, %=, ^=, &=, |=, <<, >>, &&, ||, ++ (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

TIPPre/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 dif­fer only in their return value, so if you do not use the return value, it conceptually does not mat­ter 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:

If you implement the exception-safe swap idiom for your assignment, the parameter must be pass­ed by val­ue, since “moving” a value also means modifying the “moved-from” object.

With this version of an assignment operator, it means that it actually employs the copy con­struc­tor (pass by value), and a custom swap() or std::swap function, making it dependent on the copy con­struc­tor, and will be slightly slower than an alternative version. But you do get ex­cep­tion safe­ty, if that is what you require. Or, you can overload the assignment operator as normal (as fast as pos­si­ble), and give users a safe_assign() function to use, for when they want an ex­cep­tion guarantee.

The following example is a bit long, if we were only to consider the difference between move as­sign­ment 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 com­plete, pro­duc­tion-ready class, but it has all the “good bits” we can discuss later:

copymove.cppMove Semantics
/*!@file  copymove.cpp
*  @brief Move Semantics Example
*/
#include <iostream>
#include <algorithm>

class CC {

public: // ctors & dtor.

   CC () : data_{}, size_{} {
      std::cout << " CC()[" << size_ << "] ";
      }

   CC (const CC& rhs)              //←copy ctor.
      : 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 (CC&& rhs) noexcept          //←move ctor.
      : data_{std::move(rhs.data_)}
      , size_{std::move(rhs.size_)} { 
      rhs.data_ = nullptr; rhs.size_ = 0U;
      std::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

   CC& operator= (CC& rhs) {       //←copy assignment.
      std::cout
         << " operator=(CC&)[" << size_ << "<-" << rhs.size_ << "] ";
      if (this != &rhs) {          //←not really necessary.
         CC tmp{rhs};
         std::swap(data_, tmp.data_);
         std::swap(size_, tmp.size_);
         }
      return *this;
      }

   CC& operator= (CC&& rhs)        //←move assignment.
         noexcept { 
      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

CC rfunc () {                      //←return a `CC` obj.
   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
   NL CC r1(11);                   //⇒ 1: size_t ctor.
   NL CC r2(r1);                   //⇒ 2: copy ctor.
   NL CC r3{};                     //⇒ 3: default ctor.
   NL r1 = CC(33);                 //⇒ 4: size_t ctor & move assign. 
   NL r2 = r1;                     //⇒ 5: copy assign.
   NL r3 = std::move(r1);          //⇒ 6: cast & move assign.
   NL pfunc(r2);                   //⇒ 7: copy ctor.
   NL pfunc(std::move(r3));        //⇒ 8: move ctor.
   NL r1 = rfunc();                //⇒ 7: size_t ctor & move assign.
   cout << "\n" << endl;

   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 au­to­ma­ti­cal­ly 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, caus­ing the compiler to select the move assignment operator. Since r1 is not yet out of scope, its de­struc­tor is not yet called.

On lines 7: and 8:, we call pfunc(); once with an lvalue, which causes the copy constructor to be cal­led; and the second time, with an rvalue, which causes the compiler to choose the move con­struc­tor in­stead. 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 as­sign­ment 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 con­struc­tors, move as­sign­ment 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 op­e­ra­tor superfluous and thus we would not have been able to discuss it. If you do want to im­ple­ment it, replace both assignment operator overloads with this single version:

copy-and-swap idiom alternative assignment overload
class C {
   ···
public: // overloaded operators

   CC& operator= (CC rhs) {            //←copy-and-swap idiom
      std::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 pop­u­lar 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 over­load­ed 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 “cal­lab­le”. The advantage of function objects over normal functions, is that each can carry separate state with it.

funcobjs.cppFunction Objects
/*!@file  funcobjs.cpp
*  @brief Function Objects Example
*/
#include <iostream>

class FO {                         //←‘function object’ type.
public:
   FO (int state = 0)              //←initialise state.
      : 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();
   }
FO ret (int state) {               //←returning a ‘functor’.
   return FO{state};
   }

int main () {
   using std::cout;  using std::endl;

   FO f1(123);  FO f2(456);        //←create ‘functor’ objects.

   f1();           f2();           //←call ‘functors’.
   cal(f1);        cal(f2);        //←pass ‘functors’.

   f1 = ret(111);  f2 = ret(222);  //←assign returned ‘functors’.
   f1();           f2();           //←call new ‘functors’.
   cal(FO{333});   cal(ret(444));  //←pass returned ‘functors’.

   return EXIT_SUCCESS;
   }

As you can see, we can very easily manipulate a value of type FO (Function Object), as “just a val­ue”, but abstractly, we think of it as a function we are passing around, storing, returning and cal­ling. All courtesy of the ability to overload the function call operator.

NOTECallable 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 c{};
   double d(c);                    //←implicit cast operator.
   double d = c;                   //←implicit cast operator.
   d = C{};                        //←implicit cast operator.
   d = static_cast<double>(c);     //←explictly call cast operator.

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 c{};
   double d = 123.456;
   c = 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)`.

(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”.

IMPORTANTExplicit Specifier

Note that if we did not use the explicit specifier, none of the static_casts 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 pat­tern ex­pects 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

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.hppFinal 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() : Circle(1.0) { }              //←ctor delegation
   Circle(const Circle&) = default;
   explicit inline Circle(double radius);
   ~Circle () { }

   Circle& operator= (const Circle& rhs) {
      if (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 {
      setRadius(sqrt(area / PI));
      }
   auto setCircum (double circum) -> void {
      setRadius(circum / (2.0 * PI));
      }

   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 hav­ing to per­form va­li­da­tion on the argument passed — setRadius() will throw the exception, if necessary.

Circle.cppFinal 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) {
   os << "R:" << rhs.radius_ << ", "
      << "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 class­es. We had to make it a “normal” function, since we wanted to control the first pa­ra­met­er.

main.cppFinal 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{};
   cout << "Radius?: ";
   if (!(cin >> radius)) {
      std::cerr << "Garbage input. Bailing." << endl;
      return EXIT_FAILURE;
      }

   try{
      Circle c1(radius);               //←might throw exception.
      cout << c1 << endl;              //←dump all attributes.
      c1.setRadius(c1.radius() * 2.0);

      cout << "c1's radius = " << c1.radius() << endl;
      cout << "c1's circum = " << c1.circum() << endl;
      cout << "c1's area   = " << c1.area() << endl;

      c1.setArea(c1.area());
      cout << "c1's radius = " << c1.radius() << endl;

      double r = c1;                   //←implicit cast.
      cout << "r = " << r << endl;
      Circle c2{c1};                   //←copy ctor.

      if (!(c1 < c2) && !(c2 < c1))    //←equality in terms of
         cout << "c1 == c2\n";         // less-than.
      }
   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 be­cause we have “proper” constructors and member functions to use.

NOTEAlternative Circle Implementation Example

Another implementation of a Circle class can be found in a separate article: C++ Cir­cle Code Organisation, which also contrasts it with various ways a “circle calculator” prog­ram 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 class­es via a concept called inheritance. This is syntactically very simple in C++, but to make prac­ti­cal use of it, the class hierarchy and responsibilities should be designed — es­pe­ci­al­ly con­si­der­ing 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 ob­ser­va­tion that a class is “implemented in terms of another”, you should use private in­he­ri­tan­ce (or mem­ber­ship / aggregation).

basederiv.cppTrivial 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.
      { }
   Deriv (int b, double d)
      : 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;

   Base b(111);
   Deriv d(222, 3.33);

   cout << "b::bdata        = " << b.bdata() << endl;
   cout << "d::Base::bdata  = " << d.bdata() << endl;
   cout << "d::Deriv::ddata = " << d.ddata() << endl;

   return EXIT_SUCCESS;
   }

(1) Normally, the default Base class constructor would be called before the code of the Derived 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 in­her­it 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 mem­ber in a derived class can hide a member in the base class with the same name. This is seldom done de­lib­er­ate­ly, 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 () {
      Base::member();              //←qualified lookup.
      std::cout << "Deriv::member()\n";
      }
   ···
};//class

int main () {
   ···
   Base b(11);  Deriv d{22, 33.33};

   b.member();                     //←call `Base::member()`.
   d.member();                     //←call `Deriv::member()`.
   d.Base::member();               //←call `Base::member()`.

   return EXIT_SUCCESS;
   }

The Base::member() can still be accessed by being specific about its scope, either inside the de­riv­ed 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 () {
   ···
   Base b(111);   Deriv d(222, 3.33);

   b.member(123L);                 //←calls `Base::member(long)`.
   d.member(123L);                 //←calls `Deriv::member(double)`.
   d.Base::member(123L);           //←calls `Base::member(long)`.
   d.Base::member();               //←calls `Base::member()`.
   #if defined SHOW_ERRORS
   d.member();                     //←ERROR. cannot find member.
   #endif
   ···
   }

When calling d.member(123L), the compiler does not find the best overload in Base. This is be­cause 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 con­struc­tors, 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 in­de­pen­dent of the type of inheritance used (‘···public ‹base›’, ‘···protected ‹base›’, ‘···public ‹base›’).

usingderiv.cppUsing 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 () {

   B b(111);                   //←calls `B::B(int)`.
   D d(222);                   //←calls `B::B(int)` (inherited).

   cout << d.data() << endl;   //←allowed.
   //      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 cal­led 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, un­less declared as friends.

You could also use this syntax to reduce the access. For example, if the base class Base has a public func­tion 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, de­ter­min­es its new access level.

Polymorphism

If you were to investigate the assembler-generated machine code for a function call, you will no­tice that it will be something like: CALL 12345h (call some address). This is called “sta­tic bind­ing”, 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 ad­dress is se­lect­ed from an array, and then called:

ptrtomemfuncs.cppPointer 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:
   Base () : vp_(vt_) { }
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:
   Deriv() { vp_ = reinterpret_cast<void (Base::**)()>(vt_); }
   void 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;

   Base b{}; Deriv d{};

   b.vcall(0);   b.vcall(1);       //←calls `Base::vfunc1&2()`.
   d.vcall(0);                     //←calls `Base::vfunc1()`.
   d.vcall(1);                     //←calls `Deriv::vfunc2()`.

   Base* p = &b;
   p->vcall(0);  p->vcall(1);      //←calls `Base::vfunc1&2()`.
   p = &d;
   p->vcall(0);                    //←calls `Base::vfunc1()`.
   p->vcall(1);                    //←calls `Deriv::vfunc2()`.

   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 in­it­ial­ise 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 mark­ed to appear in the table. The keyword controlling this is virtual, which leads to “virtual func­tions” 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.cppVirtual 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;

   Base b{}; Deriv d{};
   
   b.vfunc1();  b.vfunc2();        //←calls `Base::vfunc1&2()`.

   d.vfunc1();                     //←calls `Base::vfunc1()`.
   d.vfunc2();                     //←calls `Deriv::vfunc2()`.

   Base* p{&b};
   p->vfunc1();  p->vfunc2();      //←calls `Base::vfunc1&2()`.
   p = &d;
   p->vfunc1();                    //←calls `Base::vfunc1()`.
   p->vfunc2(1);                   //←calls `Deriv::vfunc2()`.

   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 “poly­mor­ph­ism” 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.

IMPORTANTVirtual 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:

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{};
   Abstract* p{&d};   p->abs_func();
   Abstract& r{ d};   r. abs_func();
···

(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 some­thing like the fol­low­ing, 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
···
   Shape* dwg[]{ // randomly, dynamically, add “shapes”.
      new Circle{}, new Rectangle{}, new Triangle{},
      new Circle{}, new Triangle{}, new Rectangle{}
      };
   for (size_t i = 0
         ; i < sizeof(dwg)/sizeof(*dwg)
         ; ++i) {
      dwg[i]->draw();                //←call one of three `draw`s.
      }
   ··· //delete memory

Forgive the C-style array, and raw new, but it is so we can emphasise that one statement, con­tain­ing the call dwg[i]->draw(), will call different implementations of the function, depending on the run­time type of the pointer, even though the compile-time type of dwg[i] is Shape*. That is clas­sic poly­mor­phism; 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:


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]