More Myths of My Blog Readers
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.
Always take the parameter by const reference (Gunter Königsmann)
You have two options when a function takes its parameter and doesn’t want to modify it.
- 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 an online compiler rextester, I can execute the program with GCC, Clang, and cl.exe (Windows). Here are the numbers for all 64-bit systems.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
GCC
Clang
cl.exe (Windows)
cl.exe behaves differently from 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 double size.
Deciding 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.
Initialization and Assignment in the Constructor are equivalent (Gunter Königsmann)
First, let me show you initialization 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 initialization but the class Bad assignment. The consequences are:
- The variable i is directly initialized in the class Good
- The variable i is default constructed and then assigned to the class Bad
The constructor initialization is, on the 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.
- constInt is not initialized and can not be assigned in the constructor.
- refToInt is not initialized.
- The class NoDefault has no default constructor because I implemented one constructor for int. When implementing a constructor, the compiler will not automatically generate a default constructor.
In the second successful compilation, I used the second commented-out constructor, which uses initialization 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 an 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 anymore. 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 critical 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 want 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 critical 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 always has a value. Tedious 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, 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, Matt Godbolt, and Honey Sukesan.
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)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!