Lesson 5 - Classes
A deeper look at classes and their features in C++.
The keyword this
The keyword this
represents a pointer to the object whose member function is being executed. It is used within a class's member function to refer to the object itself. One of its uses can be to check if a parameter passed to a member function is the object itself.
class Dummy
{
public:
bool isitme(const Dummy& param);
};
bool Dummy::isitme(const Dummy& param)
{
return (¶m == this);
}
static members
A class can contain static
members, either data or functions.
A static
data member of a class is also known as a "class variable", because there is only one common variable for all the objects of that same class, sharing the same value: i.e., its value is not different from one object of this class to another.
In fact, static
members have the same properties as non-member variables but they enjoy class scope. For that reason, and to avoid them to be declared several times, they cannot be initialised directly in the class, but need to be initialised somewhere outside it.
class Dummy
{
public:
static int n;
Dummy() { ++n; };
};
int Dummy::n=0;
Classes can also have static member functions. These represent the same: members of a class that are common to all object of that class, acting exactly as non-member functions but being accessed like members of the class. They cannot access non-static members of the class (neither member variables nor member functions), nor can use the keyword this
.
const member functions
When an object of a class is qualified as a const
object:
const MyClass myobject;
The access to its data members from outside the class is restricted to read-only, as if all its data members were const
for those accessing them from outside the class. Note though, that the constructor is still called and is allowed to initialise and modify these data members
The member functions of a const
object can only be called if they are themselves specified as const
members. To specify that a member is a const
member, the const
keyword shall follow the function prototype, after the closing parenthesis for its parameters:
int get() const {return x;}
Note that const
can be used to qualify the type returned by a member function. This const
is not the same as the one which specifies a member as const
. Both are independent and are located at different places in the function prototype:
int get() const {return x;} // const member function
const int& get() {return x;} // member function returning a const&
const int& get() const {return x;} // const member function returning a const&
Member functions specified to be const
cannot modify non-static data members nor call other non-const
member functions. In essence, const
members shall not modify the state of an object.
const
objects are limited to access only member functions marked as const
, but non-const
objects are not restricted and thus can access both const
and non-const
member functions alike.
Member functions can be overloaded on their const
’ness: i.e., a class may have two member functions with identical signatures except that one is const
and the other is not: in this case, the const
version is called only when the object is itself const
, and the non-const
version is called when the object is itself non-const
.
class MyClass
{
int x;
public:
explicit MyClass(int val) : x(val) {}
const int& get() const {return x;}
int& get() {return x;}
};
Implicit Members
Six member functions are implicitly defined under certain circumstances
Default constructor
The default constructor is the constructor called when objects of a class are declared, but are not initialised with any arguments.
If a class definition has no constructors, the compiler assumes the class to have an implicitly defined default constructor.
As soon as a class has some constructor taking any number of parameters explicitly declared, the compiler no longer provides an implicit default constructor, and no longer allows the declaration of new objects of that class without arguments.
class Example
{
std::string data;
public:
explicit Example(const std::string& str) : data(str) {}
Example() {}
const std::string& content() const {return data;}
};
Destructor
Destructors fulfil the opposite function of constructors: They are responsible for the necessary cleanup needed by a class when its lifetime ends.
Any class that allocates dynamic memory would need to have a function that releases the memory. The most error-free option would be for the function to be called automatically at the end of the object's life. To do this, we use a destructor. A destructor is a member function very similar to a default constructor: it takes no arguments and returns nothing, not even void
. It also uses the class name as its own name, but preceded with a tilde sign (~)
class Example
{
std::string* ptr;
public:
Example() : ptr{new std::string} {}
explicit Example(const std::string& str) : ptr{new std::string{str}} {}
// destructor:
~Example() {delete ptr;}
const std::string& content() const {return *ptr;}
};
Copy constructor
When an object is passed a named object of its own type as argument, its copy constructor is invoked in order to construct a copy.
A copy constructor is a constructor whose first parameter is of type reference to the class itself (usually const
qualified) and which can be invoked with a single argument of this type.
MyClass::MyClass(const MyClass&);
If a class has no custom copy nor move constructors (or assignments) defined, an implicit copy constructor is provided. This copy constructor performs a copy of its own members.
This default copy constructor may suit the needs of many classes. But shallow copies only copy the members of the class themselves, and this is not suitable for classes that perform dynamic memory management (as example above). Performing a shallow copy of a pointer copies its value (another pointer to the same memory address), but not the content itself; This could lead to an attempt to delete the same block of memory when the original and copied instances are destroyed, probably causing the program to crash on runtime. This can be solved by defining a custom copy constructor that performs a deep copy.
class Example {
std::string* ptr;
public:
explicit Example(const std::string& str) : ptr{new std::string{str}} {}
~Example() {delete ptr;}
// copy constructor:
Example(const Example& x) : ptr{new std::string{x.content()}} {}
// access content:
const std::string& content() const {return *ptr;}
};
The deep copy performed by this copy constructor allocates storage for a new string, which is initialised to contain a copy of the original object. In this way, both objects (copy and original) have distinct copies of the content stored in different locations.
Copy assignment
Objects are not only copied on construction, when they are initialised: They can also be copied on any assignment operation.
MyClass foo;
MyClass bar(foo); // object initialisation: copy constructor called
MyClass baz = foo; // object initialisation: copy constructor called
foo = bar; // object already initialised: copy assignment called
Note that baz
is initialised on construction using an equal sign, but this is not an assignment operation! (although it may look like one): The declaration of an object is not an assignment operation, it is just another of the syntaxes to call single-argument constructors.
The assignment on foo
is an assignment operation. No object is being declared here, but an operation is being performed on an existing object; foo
.
The copy assignment operator is an overload of operator=
which takes a value or reference of the class itself as parameter. The return value is generally a reference to *this
(although this is not required). For example, for a class MyClass
, the copy assignment may have the following signature:
MyClass& operator=(const MyClass&);
The copy assignment operator is also a special function and is also defined implicitly if a class has no custom copy nor move assignments (nor move constructor) defined.
The implicit version performs a shallow copy which is suitable for many classes, but not for classes with pointers to objects they handle its storage, as is the case above. In this case, not only does the class incurs the risk of deleting the pointed object twice, but the assignment creates memory leaks by not deleting the object pointed by the object before the assignment. These issues could be solved with a copy assignment that deletes the previous object and performs a deep copy.
Example& operator=(const Example& x)
{
delete ptr; // delete currently pointed string
ptr = new std::string{x.content()}; // allocate space for new string, and copy
return *this;
}
Move constructor and assignment
Similar to copying, moving also uses the value of an object to set the value to another object. But, unlike copying, the content is actually transferred from one object (the source) to the other (the destination): the source loses that content, which is taken over by the destination. This moving only happens when the source of the value is an unnamed object.
Unnamed objects are objects that are temporary in nature, and thus have not been given a name. Typical examples of unnamed objects are return values of functions or type-casts.
Using the value of a temporary object such as these to initialise another object or to assign its value, does not really require a copy: the object is never going to be used for anything else, and thus, its value can be moved into the destination object. These cases trigger the move constructor and move assignments:
The move constructor is called when an object is initialised on construction using an unnamed temporary. Likewise, the move assignment is called when an object is assigned the value of an unnamed temporary:
MyClass fn(); // function returning a MyClass object
MyClass foo; // default constructor
MyClass bar = foo; // copy constructor
MyClass baz = fn(); // move constructor
foo = bar; // copy assignment
baz = MyClass(); // move assignment
Both the value returned by fn
and the value constructed with MyClass
are unnamed temporaries. In these cases, there is no need to make a copy, because the unnamed object is very short-lived and can be acquired by the other object when this is a more efficient operation.
The move constructor and move assignment are members that take a parameter of type rvalue reference to the class itself:
MyClass(MyClass&&); // move-constructor
MyClass& operator=(MyClass&&); // move-assignment
An rvalue reference is specified by following the type with two ampersands (&&
). As a parameter, an rvalue reference matches arguments of temporaries of this type.
The concept of moving is most useful for objects that manage the storage they use, such as objects that allocate storage with new
and delete
. In such objects, copying and moving are really different operations:
- Copying from A to B means that new memory is allocated to B and then the entire content of A is copied to this new memory allocated for B.
- Moving from A to B means that the memory already allocated to A is transferred to B without allocating any new storage. It involves simply copying the pointer.
class Example
{
std::string* ptr;
public:
Example (const std::string& str) : ptr{new std::string{str}} {}
~Example () {delete ptr;}
// move constructor
Example (Example&& x) : ptr{x.ptr} {x.ptr = nullptr;}
// move assignment
Example& operator= (Example&& x)
{
delete ptr;
ptr = x.ptr;
x.ptr = nullptr;
return *this;
}
};
Compilers already optimise many cases that formally require a move-construction call in what is known as Return Value Optimisation (RVO). Most notably, when the value returned by a function is used to initialise an object. In these cases, the move constructor may actually never get called.
Note that even though rvalue references can be used for the type of any function parameter, it is seldom useful for uses other than the move constructor. Rvalue references are tricky, and unnecessary uses may be the source of errors quite difficult to track.
Implicit generation rules
The six special members functions described above are members implicitly declared on classes under certain circumstances:
Notice how not all special member functions are implicitly defined in the same cases. This is mostly due to backwards compatibility with C structures and earlier C++ versions. Each class can select explicitly which of these members exist with their default definition or which are deleted by using the keywords
default
and delete
, respectively. The syntax is either one of:function_declaration = default;
function_declaration = delete;
class Rectangle
{
int width, height;
public:
Rectangle(int x, int y) : width{x}, height{y} {}
Rectangle() = default;
Rectangle(const Rectangle& other) = delete;
int area() const {return width*height;}
};
Note that, the keyword default
does not define a member function equal to the default constructor, but equal to the constructor that would be implicitly defined if not deleted.
In general, and for future compatibility, classes that explicitly define one copy/move constructor or one copy/move assignment but not both, are encouraged to specify either delete
or default
on the other special member functions they don't explicitly define.