Skip to content

Introduction

C++ improves over C in the sense that it supports the styles of programming done using C with better type checking and more notational support (without loss of efficiency). In addition, C++ supports data abstraction, object-oriented programming, and generic programming. Like any object-oriented language, it provides better support for encapsulation and for providing higher-level abstraction.

Building C++ Classes

Guideline referefence

While C++ offers higher-level abstraction and encapsualtion through classes, it is essential to build robust and safe classes. It is crucial that any instance of a class be in a coherent state at any time of its entire lifespan. This implies that constructors, assignment operators and destructors be designed carefully. We illustrate this with a simple String class, that is first badly designed:

src/string_bad.hpp
class String {
public:
  // constructors
  String(const char* szArray);
  String(size_t size);

  // destructor
  ~String();

  // for printing content
  size_t getSize() const { return _size; }
  const char* getContent() const { return _szArray; }

private:
  char* _szArray = nullptr;
  size_t _size = 0;
};
src/string_bad.cpp
String::String(const char* szArray) {
  LOG_DBG("Construct from const char*");
  _size = strlen(szArray);
  this->_szArray = new char[_size + 1]; // +1 for null terminator
  strcpy(this->_szArray, szArray);
}

String::String(size_t size) {
  LOG_DBG("Construct from int");
  _size = size;
  _szArray = new char[_size + 1]{0}; // +1 for null terminator
}

String::~String() {
  if (this->_szArray != nullptr) {
    delete[] this->_szArray;
    this->_szArray = nullptr;
  }
  _size = 0;
}

This very simple implementation of the String class may appear appropriate, but it has may flaws:

  • Constructors are not declared explicit, so the compiler may allow unexpected implicit conversions.
  • The copy and move semantics are not defined, so the compiler adds default operations. However, the constructors, assignment operators and destructors control the lifecycle of objects, including creation, copying, moving, and destruction. Therefore, they must be defined carefully.
  • Parameters passed to constructors and methods must be checked against nullptr.
  • The use of a raw pointer as internal buffer should be replaced by a more robust smart pointer construct.

The following program demonstrates how this String class can be used in an unsafe way:

src/play_with_classes.cpp
{
    // Create a String instance on the heap, calls the String(const char*) constructor.
    badly_defined::String* s1 = new badly_defined::String("s1");
    LOG_DBG("badly_defined::s1 is %s (length %d)", s1->getContent(), s1->getSize());
    // Create a String instance on the stack, calls the String(const char*) constructor.
    badly_defined::String s2("s2");
    LOG_DBG("badly_defined::s2 is %s (length %d)", s2.getContent(), s2.getSize());
    // Create a String instance on the heap, calls the String(int size) constructor
    badly_defined::String* s3 = new badly_defined::String(10);
    LOG_DBG("badly_defined::s3 is %s (length %d)", s3->getContent(), s3->getSize());
    // Create a string instance on the stack, also calls the String(const char*) constructor!
    badly_defined::String s4 = "hello";
    LOG_DBG("badly_defined::s5 is %s (length %d)", s4.getContent(), s4.getSize());
    // Create a string instance on the stack, also calls the String(int size) constructor!
    badly_defined::String s5 = 10;
    LOG_DBG("badly_defined::s5 is %s (length %d)", s5.getContent(), s5.getSize());

    // Uncomment the following line to get a fatal error. 
    // Make sure that you understand the reason for the fatal error.
    // badly_defined::String s6 = '1234';
  }

  {
    badly_defined::String s1("s1");
    {
      // uncomment the following line to get a fatal assertion. 
      // Make sure that you understand the reason for the fatal error.
      // badly_defined::String s2 = s1;
    }

  }

A much improved version of the String class is provided below. This implementation resolves the aforementioned problems.

src/string.hpp
class String {
public:
    // constructors
    explicit String(const char *szArray);
    explicit String(size_t size);
    ~String() = default;

    // copy constructor
    String(const String &other);
    // assignment operator
    String &operator=(const String &other);
    // Move constructor — transfers ownership, no copy
    String(String &&other) noexcept;
    // Move assignment — transfers ownership, no copy
    String &operator=(String &&other) noexcept;

    // for printing content
    size_t getSize() const { return _size; }
    const char *getContent() const { return _szArray ? _szArray.get() : ""; }

private:
    std::unique_ptr<char[]> _szArray;
    size_t _size = 0;
};
src/string.cpp
String::String(const char *szArray) {
  LOG_DBG("Construct from array");
  if (szArray == nullptr) {
    _size = 0;
    _szArray = std::make_unique<char[]>(1);
    _szArray[0] = '\0';
    return;
  }
  _size = strlen(szArray);
  _szArray = std::make_unique<char[]>(_size + 1);
  strcpy(_szArray.get(), szArray);
}

String::String(size_t size) {
  LOG_DBG("Construct from size");
  _size = size;
  _szArray = std::make_unique<char[]>(_size + 1);
  _szArray[0] = '\0';
}

// Deep copy constructor — unique_ptr cannot be shared
String::String(const String &other) {
  _size = other._size;
  _szArray = std::make_unique<char[]>(_size + 1);
  strcpy(_szArray.get(), other._szArray.get());
}

// Deep copy assignment
String& String::operator=(const String &other) {
  if (this != &other) {
    _size = other._size;
    _szArray = std::make_unique<char[]>(_size + 1);
    strcpy(_szArray.get(), other._szArray.get());
  }
  return *this;
}

// Move constructor — transfers ownership, no copy
String::String(String &&other) noexcept : 
_szArray(std::move(other._szArray)), _size(other._size) {
  other._size = 0;
}

// Move assignment — transfers ownership, no copy
String& String::operator=(String &&other) noexcept {
  if (this != &other) {
    _szArray = std::move(other._szArray);
    _size = other._size;
    other._size = 0;
  }
  return *this;
}

Writing a Test Suite for every Class

To illustrate typical tests that should be written when designing a class, a complete test suite for the String class is provided here. Clone the repository and run the tests on your target device. It is important to understand the function of each test as well as the operations prohibited by this safe implementation.