13. Techniques and Use Cases

Across the book, we’ve touched on many different topics, sometimes only in a theoretical way. In this chapter, however, I grouped many of those features and demonstrated their benefits in several practical use cases.

You’ll learn about the following aspects:

  • Strong types and the explicit keyword,
  • Initializing string data members,
  • Copy and Swap Idiom as a potential simplification of copy and move operations,
  • CRTP,
  • Creating a simple resource manager (RAII) class.
  • Factory With Self-Registering Types
  • And more!

Let’s start.

Using explicit for strong types

If you recall the first chapter, I used double to indicate horsepower (hp) inside the CarInfo structure. However, we might quickly encounter a problem where we forget about the unit and treat it as Watts instead. Can we somehow limit such problematic cases?

The answer is positive, and the main idea is to wrap the data member double power in a separate class type with explicit constructors. That it will be harder to misuse it, such an approach is called Strong Typing.

Have a look at two similar wrapper types:

Ex 13.1. Strong types and area units classes. Run @Compiler Explorer
constexpr double ToWattsRatio { 745.699872 };

class HorsePower;

class WattPower {
public:
    WattPower() = default;
    explicit WattPower(double p) : power_{p} { }
    explicit WattPower(const HorsePower& h);

    double getValue() const { return power_; }
private:
    double power_ {0.};
};

class HorsePower {
public:
    HorsePower() = default;
    explicit HorsePower(double p) : power_{p} { }
    explicit HorsePower(const WattPower& w);

    double getValue() const { return power_; }
private:
    double power_ {0.};
};

As you can see, we have two types that use explicit constructors to initialize their private data members. To create an object, you have to write the correct type name explicitly, and thus it should limit the chance of mistakes.

And here is the implementation of the converting constructors as well as stream operators for easy output:

Ex 13.2. Strong Types and area units, implementation. Run @Compiler Explorer
constexpr double ToWattsRatio { 745.699872 };

class HorsePower;

class WattPower {
    /* as before */
};

class HorsePower {
    /* as before */
};

WattPower::WattPower(const HorsePower& h) 
: power_{h.getValue()*ToWattsRatio} 
{ }

HorsePower::HorsePower(const WattPower& w) 
: power_{w.getValue()/ToWattsRatio} 
{ }

std::ostream& operator<<(std::ostream& os, const WattPower& w) {
    os << w.getValue() << "W";
    return os;
}

std::ostream& operator<<(std::ostream& os, const HorsePower& h) {
    os << h.getValue() << "hp";
    return os;
}

The interface allows us to convert between various units safely.

//HorsePower hp = 10.; // not possible, copy initialization
HorsePower hp{ 10. }; // fine
WattPower w { 1. }; // fine
WattPower watts { hp }; // fine, performs the proper conversion for us!

Additionally, we have the output support that writes out the proper unit name.

We can use the solution now:

void printInfo(const CarInfo& c) {
    std::cout << c.name << ", "
              << c.year << " year, "
              << c.seats << " seats, "
              << c.power << '\n';
}

int main() {
    CarInfo firstCar{"Megane", 2003, 5, HorsePower{116}};
    printInfo(firstCar);   
    CarInfo superCar{"Ferrari", 2022, 2, HorsePower{300}};
    printInfo(superCar);
    superCar.power = HorsePower{WattPower{500000}};
    printInfo(superCar);
}

And we’ll get the following output:

Megane, 2003 year, 5 seats, 116hp
Ferrari, 2022 year, 2 seats, 300hp
Ferrari, 2022 year, 2 seats, 670.511hp

While I had to be more explicit and write the types, the code can be safer as it’s harder to type something accidentally.

Best way to initialize string data members

Content available in the full version of the book.

The copy and swap idiom

Content available in the full version of the book. ## CRTP class counter {#sectioncrtp}

Content available in the full version of the book.

Several initialization types in one class

As the demo of various initialization techniques, I’d like to show code that creates N random “application windows.”

Here are the core points of the demo:

  • A Window class contains basic parameters like name (on the title bar), width, height, and some flags (bits per pixel, visibility).
  • The demo selects a random number X and will try to generate X Window objects.
  • Each object will have a random name composed of predefined words and a random size.
  • The application prints each window using std::cout.
  • As an additional check, an InstanceCounter class counts the number of Window objects. We can use this helper to verify the correctness of the demo.

Here’s the first part that defines the Flags object:

Ex 13.5. The Flags type. Run @Compiler Explorer
struct Flags {
    unsigned bppMode_ : 4 { 0 }; // bits per pixel
    unsigned visible_ : 1 { 1 };
    unsigned extData  : 2 { 0 };
};

Here’s the main class:

Ex 13.5. The Window type. Run @Compiler Explorer
class Window : public InstanceCounter<Window> {        
    static constexpr unsigned default_width { 1028 };
    static constexpr unsigned default_height { 768 };
    static constexpr unsigned default_bpp { 8 };
    
    unsigned width_ { default_width };
    unsigned height_ { default_height };
    Flags flags_ {.bppMode_ { default_bpp } };
    std::string title_ { "Default Window" };
    
public:
    Window() = default;
    explicit Window(std::string title) : title_(std::move(title)) { }
    Window(std::string title, unsigned w, unsigned h) :
    width_(w), height_(h), title_(std::move(title)) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Window& w) {
        os << w.title_ << ": " << w.width_ << "x" << w.height_; 
        return os;
    }
};

The Window class uses several features discussed in the book:

  • NSDMI to initialize data members,
  • designated initializers from C++20, combined with NSDMI for the flags_ data member,
  • Custom constructors that offer several options to initialize the data members,
  • We inherit from InstanceCounter, so each constructor invocation for the Window will also invoke the appropriate constructor in InstanceCounter. Similarly, the InstanceCounter destructor will be nicely called from the implicit default destructor of the Window class.

And now the final demo code:

Ex 13.5. The Window type. Run @Compiler Explorer
void WindowDemo() {
    std::random_device rd;  
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(0, 20);

    const int windowCount = std::uniform_int_distribution<>(2, 10)(gen);
    std::cout << "Generating " << windowCount << " random Windows\n";

    const std::array adjs { "regular ", "empty ", "blue ", "super " };
    const std::array nouns { "app", "tool", "console", "game" };
    const std::array sizes { 1080u, 1920u, 768u, 320u, 640u, 3840u, 800u };

    std::vector<Window> windows;
    for (int i = 0; i < windowCount; ++i) {
        auto r = distrib(gen);
        auto r2 = distrib(gen);
        auto name = std::string { adjs[(r + i) % adjs.size()] } + 
                     nouns[r2 % nouns.size()];
        Window w{name, sizes[r2 % sizes.size()], 
                 sizes[r % sizes.size()]};
        windows.push_back(w);
    }

    for (const auto& w : windows)
        std::cout << w << '\n';

    std::cout << "Created " << Window::GetInstanceCounter() << " Windows\n";
}

int main() {
    WindowDemo();

    if (Window::GetInstanceCounter() != 0) {
        std::cout << Window::GetInstanceCounter() 
                  << " Windows are still alive!\n";
    }
}

Here’s the possible output:

Generating 8 random Windows
super tool: 320x320
regular tool: 320x640
super game: 1080x768
super game: 640x1080
regular tool: 1920x3840
empty tool: 1920x3840
blue game: 320x768
empty console: 320x320
Created 8 Windows

In WindowDemo, the code declares some basic data and generates a random number. Later, in the main loop, we generate random numbers to pick values from adjs, nouns, and sizes arrays. Once the data is ready, I can create a Window object and place it in the std::vector. To show the creation of the Window object, I used push_back on a vector, but we can optimize it and call emplace_back, which doesn’t need a temporary object:

windows.emplace_back(name, sizes[r2 % sizes.size()], sizes[r % sizes.size()]);

Later there’s another loop that prints all windows.

The code uses InstanceCounter as a bonus debugging facility to ensure we have the correct number of active objects. When WindowDemo() finishes, all instances should be removed, and we can double-check it inside main().

Vector like RAII object

(*) this section will be added in the future.

Factory with self-registering types

(*) this section will be added in the future.

Summary

(*) this section will be added in the future.