More Myths of My Blog Readers

Contents[Show]

Today, I conclude my story to your myths about C++. These myths are around function parameters, the initialisation of class members, and pointer versus references.

 dragon 4417431 1280

Always take the parameter by const reference (Gunter Königsmann)

When a function takes its parameter and doesn't want to modify it, you have two options.

  • Take the parameter by value (copy it)
  • Take the parameter by const reference

This was the correctness perspective, but what can be said about the performance. The C++ core guidelines are specific about performance. Let's look at the following example.

void f1(const string& s);  // OK: pass by reference to const; always cheap

void f2(string s);         // bad: potentially expensive

void f3(int x);            // OK: Unbeatable

void f4(const int& x);     // bad: overhead on access in f4()

 

Presumably, based on experience, the guidelines state a rule of thumb:

  • You should take a parameter p by const reference if sizeof(p) > 4 * sizeof(int)
  • You should copy a parameter p if sizeof(p) < 3 * sizeof(int)

Okay, now you should know how big your data types are. The program sizeofArithmeticTypes.cpp gives the answers for arithmetic types.

// sizeofArithmeticTypes.cpp

#include <iostream>

int main(){
  
    std::cout << std::endl;
    
    std::cout << "sizeof(void*): " << sizeof(void*) << std::endl;  
    
    std::cout << std::endl;
 
    std::cout << "sizeof(5):  "  << sizeof(5)   << std::endl;
    std::cout << "sizeof(5l): "  << sizeof(5l)  << std::endl;
    std::cout << "sizeof(5ll): " << sizeof(5ll) << std::endl;
    
    std::cout << std::endl;
    
    std::cout << "sizeof(5.5f): " << sizeof(5.5f) << std::endl;
    std::cout << "sizeof(5.5): "  << sizeof(5.5)  << std::endl; 
    std::cout << "sizeof(5.5l): " << sizeof(5.5l) << std::endl;       
    
    std::cout << std::endl;
            
}

 

sizeof(void*) returns if it is a 32-bit or a 64-bit system. Thanks to online compiler rextester, I can execute the program with GCC, Clang, and cl.exe (Windows). Here are the numbers for all 64-bit systems.

GCC

sizeofGCC

Clang

sizeofClang

cl.exe (Windows)

sizeofVC

cl.exe behaves differently to GCC and Clang. A long int has only 4 bytes, and a long double has 8 bytes. On GCC and Clang, long int and long double have the double size.

To decide, when to take the parameter by value or by const reference is just math. If you want to know the exact performance numbers for your architecture, there is only one answer: measure.

Initialisation and Assignment in the Constructor are equivalent (Gunter Königsmann)

First, let me show you initialisation and assignment in the constructor.

class Good{  
    int i;
public:
    Good(int i_): i{i_}{} 
};

class Bad{  
    int i;
public:
    Bad(int i_): { i = i_; } 
};

 

The class Good uses initialisation but the class Bad assignment. The consequences are:

  • The variable i is directly initialised in the class Good
  • The variable i is default constructed and then assigned to in the class Bad

 The constructor initialisation is, on one hand, slower but does not work on the other hand for const members, references, or members which can not be default-constructed possible.

// constructorAssignment.cpp

struct NoDefault{
    NoDefault(int){};
};

class Bad{
    const int constInt;
    int& refToInt;
    NoDefault noDefault;
public:
    Bad(int i, int& iRef){
        constInt = i;
        refToInt = iRef;
    }
    // Bad(int i, int& iRef): constInt(i), refToInt(iRef), noDefault{i} {}
};

int main(){
    
    int i = 10;
    int& j = i;
  
    Bad bad(i, j);
  
}

 

When I try to compile the program, I get three different errors.

  1. constInt is not initialised and can not be assigned in the constructor.
  2. refToInt is not initialised.
  3. The class NoDefault has no default constructor because I implemented one constructor for int. When you implement a constructor, the compiler will not automatically generate a default constructor.

 

constructorAssignment

In the second successful compilation, I used the second commented out constructor which uses initialisation instead of assignment.

The example used references instead of raw pointers for a good reason.

You need Raw Pointers in your Code (Thargon110)

Motivated by a comment from Thargon110, I want to be dogmatic: NNN. What? I mean No Naked New. From an application perspective, there is no reason to use raw pointers. If you need a pointer like semantic, put your pointer into a smart pointer (You see: NNN) and you are done.

In essence, C++11 has a std::unique_ptr for exclusive ownership and a std::shared_ptr for shared ownership. Consequently, when you copy a std::shared_ptr, the reference counter is incremented, and when you delete the std::shared_ptr, the reference counter is decremented. Ownership means, that the smart pointer keeps track of the underlying memory and releases the memory if it is not necessary any more. The memory is not necessary any more in the case of the std::shared_ptr when the reference counter becomes 0.

So memory leaks are gone with modern C++. Now I hear your complaints. I'm happy to destroy them.

  • Cycles of std::shared_ptr can create a memory leak because the reference counter will not become 0. Right, put a std::weak_ptr in-between to break the cyclic reference: std::weak_ptr.
  • A std::shared_ptr has a management overhead and is, therefore, more expensive than a raw pointer. Right, use a std::unique_ptr.
  • A std::unique_ptr is not comfortable enough because it can't be copied. Right, but a std::unique_ptr can be moved.

 The last complaint is quite dominant. A small example should make my point:

 

// moveUniquePtr.cpp

#include <algorithm>
#include <iostream>
#include <memory>
#include <utility>
#include <vector>

void takeUniquePtr(std::unique_ptr<int> uniqPtr){          // (1)
    std::cout << "*uniqPtr: " << *uniqPtr << std::endl;
}

int main(){
  
    std::cout << std::endl;
  
    auto uniqPtr1 = std::make_unique<int>(2014);
    
    takeUniquePtr(std::move(uniqPtr1));                    // (1)
    
    auto uniqPtr2 = std::make_unique<int>(2017);
    auto uniqPtr3 = std::make_unique<int>(2020);
    auto uniqPtr4 = std::make_unique<int>(2023);
    
    std::vector<std::unique_ptr<int>> vecUniqPtr;
    vecUniqPtr.push_back(std::move(uniqPtr2));             // (2)
    vecUniqPtr.push_back(std::move(uniqPtr3));             // (2)
    vecUniqPtr.push_back(std::move(uniqPtr4));             // (2)
    
    std::cout << std::endl;
    
    std::for_each(vecUniqPtr.begin(), vecUniqPtr.end(),    // (3)
                  [](std::unique_ptr<int>& uniqPtr){ std::cout <<  *uniqPtr << std::endl; } );
    
    std::cout << std::endl;
    
}

 

The function takeUniquePtr in line (1) takes a std::unique_ptr by value. The key observation is that you have to move the std::unique_ptr inside. The same argument holds for the std::vector<std::unique_ptr<int>> (line 2). std::vector as all containers of the standard template library wants to own its elements but to copy a std::unique_ptr is not possible. std::move solves this issue. You can apply an algorithm such as std::for_each on the std::vector<std::unique_ptr<int>> (line 3) if no copy semantic is used.

Use References instead of Raw Pointers

In the end, I want to refer to the key concern of Thargon110. Admittedly, this rule is way more important in classical C++ without smart pointers because smart pointers are in contrast to raw pointers owners.

Use a reference instead of a pointer because a reference has always a value. Boring checks such as the following one are gone with references. 

 

if(!ptr){
   std::cout << "Something went terrible wrong" << std::endl;
   return;
}
std::cout << "All fine" << std::endl;

 

Additionally, you can forget the check. References behave just as constant pointers.

What's next?

The C++ core guidelines define profiles. Profiles are a subset of rules. They exist for type safety, bounds safety, and lifetime safety. They will be my next topic.

 

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Dimitrov Tsvetomir, Leo Goodstadt, Eduardo Velasquez, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, and Michael Young.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, and Bhushan Ivatury.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

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.

New

Contact Me

Modernes C++,

RainerGrimmSmall

 
 

 

 

My Newest E-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)

Blog archive

Source Code

Visitors

Today 1676

Yesterday 7541

Week 24602

Month 194019

All 7461859

Currently are 183 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments