Designing an ideal class in C++
Definitive guide to creating a readable and efficient class
Designing a user-defined class in C++ is not straight forward as Modern C++ provides a lot of new features which is often hard to remember all of them, but cannot be ignored as it makes your code more manageable, efficient, secure and readable. We should take into consideration all of these features while designing a class if we want to reap the benefits of what it offers.
If you are new to C++, it often happens that you miss some of the features that you want to incorporate or may get confused about why the compiler is throwing error for code which looks okay at first glance. Hence this can be used as a guideline and as well as a checklist before defining a class.
#1: Understand the Rule of Zero and Five
In C++, there are some Special Member Functions(SMF), which gets generated by the compiler by default if not defined by the user. These are :
- Default Constructor
- Destructor
- Copy Constructor
- Copy Assignment
- Move Constructor
- Move Assignment
Rule of Zero — The Ro0 states that if a class does not manage any resource explicitly, it need not define any of the SMF to handle resources as the compiler-generated functions will handle it automatically. The idea here is to make use of the standard library to manage resources such as using Smart Pointers for memory and for files ofstream/ifstream. This makes you write less code and hence less error-prone your code is.
#include <memory>
#include <vector>class A {
private:
double x = 5.5;
std::vector<int> v = {10, 20};
};class B{
public:
std::unique_ptr<int> p = std::make_unique<int>(10);
};
The class A
does not implement custom copy and move constructor, yet correctly supports copies and moves without any memory leaks. For primitive data type, x
it copies the value and for vector, all the elements are copied.
In class B
only move operation is supported as p
is a unique_ptr which does not allow copy operation, but correctly supports move operation, which will transfer the ownership of the dynamically allocated resource.
Rule of Five — Before we learn about Ro5, we must know and understand Ro3, which states that if we require destructor, copy constructor or copy assignment operator in our class, then we certainly need all of them. We usually define them when we want to explicitly handle the resource for non-class types such as raw pointers, file descriptors, etc. As compiler-generated destructor does nothing and copy constructor/assignment only do shallow copy, we need to do it on our own by defining all of these three SMF.
Declaration of any of these functions disables the generation of move constructor/assignment and if it is desirable in your class, then we must define all of the five special member functions. This is the Rule of Five. If you are interested in learning in more depth, you can go through this article published in www.feabhas.com
#2: Explicitly create defaulted SMF using the “default” specifier.
The concept behind the rule of zero is good, but care should be taken as if you declare any of these functions explicitly, as it might disable the generation of other functions that you may need. For example, if you declare a destructor for some reason, it will disable the generation of move constructor/assignment.
From this table we can see how defining your own custom Special Member Functions (SMF) can modify the behavior of the compiler on how it generates other SMF.
If a defaulted SMF does not get created, you can force the compiler to generate it using the default
specifier.
class A {
A(A&&) = default; // create the default move constructor
A& operator=(A&&) = default;
~A() {}
};
As the class A
in this example would not generate default constructor, you can explicitly generate the default constructor using the default
specifier.
#3: Use Resource Acquisition Is Initialization (RAII) technique to manage the life cycle of resources.
RAII is a technique of writing safer code for usage of system resources such as memory, file handles, mutexes, database handles, network sockets by encapsulating it within a class, wherein the constructor acquires the resource(usually) and when the resource goes out of scope, its destructor is called automatically and releases the resource. This guarantees that even if an exception occurs or you forgot to release it explicitly, the resource is released properly.
Let us see with some examples of what issues we may face without RAII.
1. File Operation
FILE *fptr;fptr = fopen("filename","w");// Some other operations
// Exception here will not close the file properly
...close(fptr); // May not get called in case of exception2. Memory
int *ptr = new int[40];// Some other operations
// Exception here can cause memory leak...
...delete []ptr;
If an exception occurs appropriate calls to release resources may not be invoked unless in exception handling block we do not explicitly write code to release the resource. Which is untidy and sometimes we might forget to write it. It is in these kinds of situations RAII is very useful. It relinquishes the resource properly and code is more manageable and clean.
Let us look through an example to see RAII in action:
class FileHandler {
private:
File *fptr = nullptr;public:
FileHandler(string filename, string mode) {
fptr = fopen(filename, mode);
} ~FileHandler() {
if (fptr) {
fclose(fptr);
}
}
};int main() {
FileHandler file1("filename", "w");
// Some operations
// Now even if exception occur here we can be sure file will be
// closed properly eventually.
}
This simple example shows how we can write a class to encapsulate file handling to manage the lifetime of the file resource. Here the constructor handles the opening of the file and the destructor closes the file once the object goes out of scope either due to exception or normal exit.
#4: To prevent a function from being called, use the “delete” specifier after the function declaration
class A {
A(const A &) = delete; // Disable copy constructor
A(A &&) = delete; // disable use of move constructor
};
It is better than making functions private as it makes the method inaccessible even from contexts that can see private
methods (i.e. within the class and its friends). This removes any uncertainty when you’re reading the code.
It can be used for normal function as well. It can help catch unintended use. For example, if we have below functions:
void print(int x);
void print(string str);
Calling print(‘i’)
will implicitly convert char to int and call void print(int x)
function, when likely this was intended:print("i")
. This can be prevented by making:
void print(char ch) = delete;
#5: Inherit parameterized constructors from a base class using “using” keyword
Before C++11, there was no way to inherit constructors. For example:
class A
{
public:
explicit A(int x) {}
};
class B: public A
{
};
int main(void)
{
B *b = new B(5);
delete b;
}
This code would not compile on C++03 standard compiler. You would need to explicitly call A’s constructor to make it work.
With C++11, all the parameterized constructors of the base class can be inherited into derived class except for the ones where derived class provides its own having matching function signature.
class A
{
public:
explicit A(int x) {}
};
class B: public A
{
public:
using A::A; //Inherits parameterized constructor from class A
};
int main(void)
{
B *b = new B(5);
delete b;
}
#6: Correctly overriding virtual functions using “override” specifier
While overriding a virtual function from base class if function mismatch happens, you may hide instead of overriding it. For example:
class Base {
public:
Base();
virtual void sum(int a);
};class Derived {
public:
Derived();
void sum(int* a); // function signature mismatch
};
The function sum
was supposed to override the virtual function from the base class, but due to function mismatch, it is hidden to Derived
class.
This can be prevented by using the override
specifier to indicate that you intend to override a virtual function.
class Derived {
public:
Derived();
void sum(int* a) override; // Compiler will throw a error now!
};
#7: Use “const” modifier for functions which should not be allowed to modify the object.
Some function should not modify the object on which it is called. Using const
specifier at the end of function declaration, it guarantees that the compiler will throw an error if the function tries to modify any of the object’s variables.
class Foo {
int a;
private:
int modify(int args) const; // cannot modify member variable 'a'
};
There is an exception to the rule. Sometimes it is required that few of the member variables be allowed to be modified by const member function. This can be done by marking the class variable as mutable
.
#8: Use “explicit” modifier for no implicit conversions
In C++, any constructor that takes a single argument acts as a blueprint for converting a value of that argument type to the class type.
class Square {
private:
int len;
public:
Square(int l);
};
In this example, Square(int l)
constructor act as a blueprint for this class. Hence this is a valid statement:
Sqaure sq;
sq = 4; //Implicitly calls Square(int l) constructor
While this is a nice feature to have, but sometimes it is not desirable. To turn it off explicitly use the explicit
keyword before the constructor declaration.
explicit Square(int l);
#9: Use the “final” specifier for classes that shall not be inherited and for virtual functions that shall not be overridden.
final
specifier has two use case:
- Mark class as final to prevent it from getting inherited which can result in some performance improvement as virtual calls would get resolved at compile time itself.
class base {
public:
virtual void func() const = 0;
};
class derived final : base {
public:
void func() const override {}
};
Here, by making derived
final, the compiler can see that func
will only ever get called from derived::func
, hence it can be resolved at compile time.
- Prevent a virtual function in class to be overridden. Its use case can be seen in the below example:
class Base {
public:
virtual void func();
};
class Derived1 : base {
public:
void func() final; // Virtual as it overrides Base::func
};
class Derived2 : Derived1 {
public:
//void func(); // Error: cannot override!
};
Since you cannot mark func
function in Derived1
as non-virtual, you have to use the final
in order to prevent it from getting overridden.
#10: Declare member function “noexcept” if it does not throw an exception or you don’t care in case of exception.
By declaring a function, a method, or a lambda-function as noexcept
, you specify that these do not throw an exception and if they throw, you do not care and let the program just crash.
vector<string> collect(istream& is) noexcept
{
vector<string> res;
for (string s; is >> s;)
res.push_back(s);
return res;
}
In this example, the collect function may run out of memory and if it does the program will crash. Unless the program is crafted for memory exhaustion, it is better to declare it noexcept
.
Declaring a function noexcept
helps optimizers by reducing the number of alternative execution paths. It also speeds up the exit after failure.