Designing an ideal class in C++

Definitive guide to creating a readable and efficient class

Anupam Mazumdar
A dev’s life
8 min readMar 17, 2020

--

Image courtesy: hackaday.com

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.

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.

Image Credit: Howard Hinnant

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.

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.

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:

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

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:

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:

#5: Inherit parameterized constructors from a base class using “using” keyword

Before C++11, there was no way to inherit constructors. For example:

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.

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

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.

#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.

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.

In this example, Square(int l) constructor act as a blueprint for this class. Hence this is a valid statement:

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.

#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.

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:

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.

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.

--

--