C++ Core Guidelines: Rules for Conversions and Casts
This is a cross-post from www.ModernesCpp.com.
What have narrowing conversion and casts in common? They are very often the source of errors; therefore, I will today write about errors.
Here are the rules from the guidelines.
- ES.46: Avoid narrowing conversions
- ES.48: Avoid casts
- ES.49: If you must use a cast, use a named cast
- ES.50: Don’t cast away const
- ES.55: Avoid the need for range checking
Narrowing conversion is a conversion of a value including the loss of its precision. Most of the times that is not what you want.
ES.46: Avoid narrowing conversions
Here are a few examples from the guidelines.
double d = 7.9;
int i = d; // bad: narrowing: i becomes 7
i = (int) d; // bad: we're going to claim this is still not explicit enough
void f(int x, long y, double d)
{
char c1 = x; // bad: narrowing
char c2 = y; // bad: narrowing
char c3 = d; // bad: narrowing
}
If you want to have narrowing conversion, you should do it explicitly not implicitly according to the Python rule from The Zen of Python: Explicit is better than implicit. The guideline support library (GSL) has two cast to express your intent: gsl::narrow_cast and gsl::narrow.
double d = 7.9;
i = narrow_cast<int>(d); // OK (you asked for it): narrowing: i becomes 7
i = narrow<int>(d); // OK: throws narrowing_error
The gsl::narrow_cast performs the cast and the gsl::narrow cast throws an exception if a narrowing conversion happens.
Most of the times, a narrowing conversion happend secretely. How can you protect yourself from this? Use the power of the curly braces:
// suppressNarrowingConversion.cpp
void f(int x, long y, double d){
char c1 = {x};
char c2 = {y};
char c3 = zj3nl9r5;
}
int main(){
double d = {7.9};
int i = zj3nl9r5;
f(3, 3l, 3.0);
}
All initialisations are put into curly braces. According to the C++11 standard, the compiler has to warn you if a narrowing conversion happens.
Explict is better than implicit. This will not hold a C-cast.
ES.48: Avoid casts
Let's see what will happen if we screw up the type system.
// casts.cpp
#include <iostream>
int main(){
double d = 2;
auto p = (long*)&d;
auto q = (long long*)&d;
std::cout << d << ' ' << *p << ' ' << *q << '\n';
}
Neither the result with the Visual Studio compiler
nor the result with the gcc or the clang compiler is promising.
What is bad about the C-cast? You don't see which cast is actually performed. If you perform a C-cast, a combination of casts will be applied if necessary. Roughly speaking, a C-cast starts with a static_cast, continues with a const_cast, and finally performs a reinterpret_cast.
Of course, you know how I will continue: explicit is better than implicit.
ES.49: If you must use a cast, use a named cast
Including the GSL, C++ offers eight different named casts. Here are they including a short description:
- static_cast: conversion between similar types such as pointer types or numeric types
- const_cast: adds or removes const or volatile
- reinterpret_cast: converts between pointers or between integral types and pointers
- dynamic_ cast: converts between polymorph pointers or references in the same class hierarchy
- std::move: converts to a rvalue reference
- std::forward: converts to a rvalue reference
- gsl::narrow_cast: applies a static_cast
- gsl::narrow: applies a static_cast
What? std::move and std::forward are casts? Le's have a closer look at the internas of std::move:
static_cast<std::remove_reference<decltype(arg)>::type&&>(arg)
First the type of the argument arg is determined by decltype(arg). Then all reference are removed and two new references added. The function std::remove_reference is from the type-traits library. I have already written a few posts to the type-traits library. At the end we will always get a rvalue reference.
Casting away const is undefined behaviour.
ES.50: Don’t cast away const
Let me be more specific. Casting away const is undefined behaviour if the underlying object such as constInt is not mutable.
const int constInt = 10;
const int* pToConstInt = &constInt;
int* pToInt = const_cast<int*>(pToConstInt);
*pToInt = 12; // undefined behaviour
If you don't believe me, their is a footnote in the C standard [ISO/IEC 9899:2011] (subclause 6.7.3, paragraph 4) which is also relevant for the C++ standard: The implementation may place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used.
Did I mention mutable? mutable is one of the most unknown features in C++. mutable allows you to differentiate between bitwise and logical constness. What?
Imagine you want to implement the interface to a telephone book. For simplicity reasons, the entries should be in an std::unordered_map.
// teleBook.cpp
#include <iostream>
#include <string>
#include <unordered_map>
std::unordered_map<std::string, int> getUpdatedTelephoneBook(){
// generate a new, updated telephone book
return {{"grimm",123}, {"huber", 456}, {"schmidt", 321}};
}
class TelephoneBook{
public:
int getNumber(const std::string& name) const {
auto ent = cache.find(name);
if(ent != cache.end()){
return ent->second;
}
else{
cache = getUpdatedTelephoneBook(); // (2)
return cache[name];
}
}
private: // (1)
std::unordered_map<std::string, int> cache = {{"grimm",123}, {"huber", 456}};
};
int main(){
std::cout << std::endl;
TelephoneBook telBook; // (3)
std::cout << "grimm " << telBook.getNumber("grimm") << std::endl;
std::cout << "schmidt " << telBook.getNumber("schmidt") << std::endl;
std::cout << std::endl;
}
My telephone book (1) is extremly small. Usually, a telephone book is quite big and updating it is quite an expensive operation (2). This means updating a printed telephone book will happen only once a year in Germany. From the conceptional view, the inqueries to the teleBook (3) should be const. This is not possible, because the unordered_map is modified in the method getNumber. Here is the proof in red ellipses.
The qualifier mutable allows you to differentiate between bitwise and logical constness. The telBook is logical but not bitwise const.
// teleBook.cpp
#include <iostream>
#include <string>
#include <unordered_map>
std::unordered_map<std::string, int> getUpdatedTelephoneBook(){
// generate a new, updated telephone book
return {{"grimm",123}, {"huber", 456}, {"schmidt", 321}};
}
class TelephoneBook{
public:
int getNumber(const std::string& name) const {
auto ent = cache.find(name);
if(ent != cache.end()){
return ent->second;
}
else{
cache = getUpdatedTelephoneBook(); // (2)
return cache[name];
}
}
private: // (1)
mutable std::unordered_map<std::string, int> cache = {{"grimm",123}, {"huber", 456}};
};
int main(){
std::cout << std::endl;
const TelephoneBook telBook; // (3)
std::cout << "grimm " << telBook.getNumber("grimm") << std::endl;
std::cout << "schmidt " << telBook.getNumber("schmidt") << std::endl;
std::cout << std::endl;
}
I just added const (3) to the telBook and mutable to the cache (1) and the program behaves as expected.
ES.55: Avoid the need for range checking
I can make it short. By using the range-based for-loop or algorithms of the STL, there is no need to check the range.
std::array<int, 10> arr = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
std::sort(arr.begin(), arr.end());
for (auto a : arr) {
std::cout << a << " ";
}
// 0 1 2 3 4 5 6 7 8 9
What's next?
In the next post to expressions, I will write about std::move, new and delete and slicing. Slicing is propably one of the darkest corners of C++. So, stay tuned.