C++ Core Guidelines: More Non-Rules and Myths

Contents[Show]

Demystifying non-rules and myths in C++ is a laborious but absolutely necessary job. The goal is simple: use the powerful tool C++ appropriately.

 sao jorge 1503439 1280

By the way, did you noticed that my family name qualifies me, in particular, to write about this demystification? Anyway, here are the rules from the C++ core guidelines for today.

NR.5: Don’t: Don’t do substantive work in a constructor; instead use two-phase initialization

Obviously, this is the job a constructor: After the constructor is executed you should have a fully initialised object. For that reason, the following code snippet from the guidelines is bad. 

class Picture
{
    int mx;
    int my;
    char * data;
public:
    Picture(int x, int y)
    {
        mx = x,
        my = y;
        data = nullptr;
    }

    ~Picture()
    {
        Cleanup();
    }

    bool Init()
    {
        // invariant checks
        if (mx <= 0 || my <= 0) {
            return false;
        }
        if (data) {
            return false;
        }
        data = (char*) malloc(x*y*sizeof(int));
        return data != nullptr;
    }

    void Cleanup()                    // (2)
    {
        if (data) free(data);
        data = nullptr;
    }
};

Picture picture(100, 0); // not ready-to-use picture here
// this will fail..                   // (1)
if (!picture.Init()) {
    puts("Error, invalid picture");
}
// now have a invalid picture object instance.

 

picture(100, 0) is not fully initialised and, therefore, all operation on the picture inline (1) operate on an invalid picture. The solution to this problem is as simple as effective: put all initialisation into the constructor.

 

class Picture
{
    size_t mx;
    size_t my;
    vector<char> data;

    static size_t check_size(size_t s)
    {
        // invariant check
        Expects(s > 0);
        return s;
    }

public:
    // even more better would be a class for a 2D Size as one single parameter
    Picture(size_t x, size_t y)
        : mx(check_size(x))
        , my(check_size(y))
        // now we know x and y have a valid size
        , data(mx * my * sizeof(int)) // will throw std::bad_alloc on error
    {
        // picture is ready-to-use
    }
    // compiler generated dtor does the job. (also see C.21)
};

 

Additionally, data is in the second example a std::vector and not a raw pointer. This means the Cleanup function (line 2) from the first example is not necessary anymore because the compiler will automatically clean up. Thanks to the static function check_size, the constructor can validate its arguments. But this is not the end of the benefits modern C++ gives up. 

Often you use constructors to set the default behaviour of an object. Don't do it. Directly set the default behaviour of an object in the class body. For example, compare the following classes Widget and WidgetImpro.

 

// classMemberInitialiserWidget.cpp

#include <iostream>

class Widget{
  public:
    Widget(): width(640), height(480), frame(false), visible(true) {}
    explicit Widget(int w): width(w), height(getHeight(w)), frame(false), visible(true){}
    Widget(int w, int h): width(w), height(h), frame(false), visible(true){}

    void show(){ std::cout << std::boolalpha << width << "x" << height
                           << ", frame: " << frame << ", visible: " << visible
                           << std::endl;
     }
  private:
    int getHeight(int w){ return w*3/4; }
    int width;
    int height;
    bool frame;
    bool visible;
};

class WidgetImpro{
  public:
    WidgetImpro(){}
    explicit WidgetImpro(int w): width(w), height(getHeight(w)){}
    WidgetImpro(int w, int h): width(w), height(h){}

    void show(){ std::cout << std::boolalpha << width << "x" << height
                           << ", frame: " << frame << ", visible: " << visible
                           << std::endl;
    }

  private:
    int getHeight(int w){ return w * 3 / 4; }
    int width = 640;
    int height = 480;
    bool frame = false;
    bool visible = true;
};


int main(){

  std::cout << std::endl;

  Widget wVGA;
  Widget wSVGA(800);
  Widget wHD(1280, 720);

  wVGA.show();
  wSVGA.show();
  wHD.show();

  std::cout << std::endl;

  WidgetImpro wImproVGA;
  WidgetImpro wImproSVGA(800);
  WidgetImpro wImproHD(1280, 720);

  wImproVGA.show();
  wImproSVGA.show();
  wImproHD.show();

  std::cout << std::endl;

}

 

Both classes behave the same.

 classMemberInitialiserWidget

The difference is that the constructors for the class WidgetImpro are way more comfortable to use and to extend. When you add a new variable to both classes, you have in the case of WidgetImpro only to edit one place, but each constructor in the case of the class Widget class is affected. Here is the picture I have in mind when I design a new class: Define the default behaviour of each object in the class body. Use explicit constructors to vary the default behaviour.

Done? No!

Often you use an init function to put common initialisation or validation stuff into one place. Fine, you follow the important DRY (Don't Repeat Yourself) principle, but you automatically break the other important principle, that your object should be fully initialised after the constructor call. How can you solve this riddle? Quite easy. Since C++11 we have constructor delegation. This means to put the common initialisation and validation stuff into one smart constructor and use the other constructors as a kind of wrapper-constructors. Here is my idea translated to code.

 

// constructorDelegation.cpp

#include <cmath>
#include <iostream>

class Degree{
public:
  explicit Degree(int deg){                // (2)
    degree = deg % 360;
    if (degree < 0) degree += 360;
  }
  
  Degree() = default;
                                          // (3)
  explicit Degree(double deg):Degree(static_cast<int>(ceil(deg))) {}  

  int getDegree() const { return degree; }

private:
  int degree{};                           // (1)
};

int main(){

  std::cout << std::endl;

  Degree degree;
  Degree degree10(10);
  Degree degree45(45);
  Degree degreeMinus315(-315);
  Degree degree405(405);
  Degree degree44(44.45);

  std::cout << "Degree(): " << degree.getDegree() << std::endl;
  std::cout << "Degree(10): " << degree10.getDegree() << std::endl;
  std::cout << "Degree(45): " << degree45.getDegree() << std::endl;
  std::cout << "Degree(-315): " << degreeMinus315.getDegree() << std::endl;
  std::cout << "Degree(405): " << degree405.getDegree() << std::endl;
  std::cout << "Degree(44.45): " << degree44.getDegree() << std::endl;

  std::cout << std::endl;

}

 

The expression int degree{} (line) 1 value-initialises the degree to 0. The constructor in line 2 is quite smart. It transforms each degree to the unit circle. The constructor, taking a double, uses this constructor. For completeness, here is the output of the program:

constructorDelegation

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Stay informed about my mentoring programs.

 

 

Subscribe via E-Mail.

NR.6: Don’t: Place all cleanup actions at the end of a function and goto exit

Okay, we can do better as the following code from the guidelines:

 

void do_something(int n)
{
    if (n < 100) goto exit;
    // ...
    int* p = (int*) malloc(n);
    // ...
exit:
    free(p);
}

 

By the way. Do you spot the error? The jump goto exit bypasses the definition of the pointer p.

What I often saw in legacy C-code was code structures like this.

// lifecycle.c

#include <stdio.h>
void initDevice(const char* mess){ printf("\n\nINIT: %s\n",mess); } void work(const char* mess){ printf("WORKING: %s",mess); } void shutDownDevice(const char* mess){ printf("\nSHUT DOWN: %s\n\n",mess); } int main(void){ initDevice("DEVICE 1"); work("DEVICE1"); { initDevice("DEVICE 2"); work("DEVICE2"); shutDownDevice("DEVICE 2"); } work("DEVICE 1"); shutDownDevice("DEVICE 1"); return 0; }

 

This is very error-prone but also a typical code. Each usage of the device consists of three steps: initialisation, usage, and release of the device. Honestly, this is the job of RAII.

 

// lifecycle.cpp

#include <iostream>
#include <string> class Device{ private: const std::string resource; public: Device(const std::string& res):resource(res){ std::cout << "\nINIT: " << resource << ".\n"; } void work() const { std::cout << "WORKING: " << resource << std::endl; } ~Device(){ std::cout << "SHUT DOWN: "<< resource << ".\n\n"; } }; int main(){ Device resGuard1{"DEVICE 1"}; resGuard1.work(); { Device resGuard2{"DEVICE 2"}; resGuard2.work(); } resGuard1.work(); }

 

Initialise the resource in the constructor and release it in the destructor. First, you can not forget to initialise the object, and secont, the compiler takes care of the release of the resource. The output of both programs is equivalent:

lifecycle

 

You can find more information on RAII in my previous post: C++ Core Guidelines: When RAII breaks.

More Myths

I'm sure, this is not the end of the fight and you know more non-rules and myths about C++. Please write a letter to This email address is being protected from spambots. You need JavaScript enabled to view it.. Describe the myth and present if possible your solution. I try to make a post out of your content and add - if you like it - your name. I'm totally curious about your ideas.

What's next

Only one rule to non-rules and myths is left in the C++ core guidelines. I hope for your input.

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, and Ann Shatoff.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

  • C++ - The Core Language
  • C++ - The Standard Library
  • C++ - Compact
  • C++11 and C++14
  • Concurrency with Modern C++
  • Design Pattern and Architectural Pattern with C++
  • Embedded Programming with Modern C++
  • Generic Programming (Templates) with C++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

Tags: Myths

Mentoring

Stay Informed about my Mentoring

 

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 2671

Yesterday 5317

Week 2671

Month 146842

All 11627996

Currently are 267 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments