sao jorge 1503439 1280

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

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

 sao jorge 1503439 1280

By the way, did you notice 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

This is the job of a constructor: After the constructor is executed, you should have a fully initialized object. For that reason, the following code snippet from the guidelines is terrible. 

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 initialized; therefore, all operations on the picture inline (1) operate on an invalid picture. The solution to this problem is as simple as effective: put all initialization 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. 

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    Often you use constructors to set the default behavior of an object. Please don’t do it. Directly set the default behavior 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 extend. When you add a new variable to both classes, in the case of WidgetImpro, you have 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 behavior of each object in the class body. Use explicit constructors to vary the default behavior.

    Done? No!

    Often you use an init function to put standard initialization or validation stuff into one place. You follow the necessary DRY (Don’t Repeat Yourself) principle but automatically break the other important principle that your object should be fully initialized after the constructor call. How can you solve this riddle? Quite easy. Since C++11, we have constructor delegation. This means putting the standard initialization and validation stuff into one intelligent constructor and using 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-initialized the degree to 0. The constructor in line 2 is quite intelligent. It transforms each degree into a unit circle. The constructor, taking a double, uses this constructor. For completeness, here is the output of the program:

    constructorDelegation

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

    Okay, we can do better with 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 device usage consists of three steps: initialization, usage, and release. 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(); }

     

    Initialize the resource in the constructor and release it in the destructor. First, you can not forget to initialize the object, and second, 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 rainer.grimm@modernescpp.de. Describe the myth and present, if possible, your solution. I try to make a post from your content and add your name if you like it. I’m inquisitive 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, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, 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, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.

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

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *