Cippi admires her diamond.
I have a bit of an issue: On the one hand, pretty much everything in the five rules about constants and immutability has been covered in previous rules. On the other hand, writing your software using as much constant and immutable data as possible solves many challenges by design. Therefore, this section recapitulates the rules on constness and refers to previous rules when they provide additional value. In the end, const
, constexpr
, and immutability are such essential ideas that they should have an explicit place in this book about the C++ Core Guidelines.
const
This rule is straightforward. You can make a value of a built-in data type or an instance of a user-defined data type const
. The effect is the same. If you want to change it, you get a compiler error.
struct Immutable { int val{12}; }; int main() { const int val{12}; val = 13; // assignment of read-only variable 'val' const Immutable immu; immu.val = 13; // assignment of member 'Immutable::val' // in read-only object }
Casting away const
may cause undefined behavior if the underlying object is const
: “ES.50: Don’t cast away const
.”
Declaring member functions const
has two obvious benefits. An immutable object can only invoke const
member functions, and const
member functions cannot modify the underlying object. Here is a short example that includes the error messages from GCC:
struct Immutable { int val{12}; void canNotModify() const { val = 13; // assignment of member 'Immutable::val' // in read-only object } void modifyVal() { val = 13; } }; int main() { const Immutable immu; immu.modifyVal(); // passing 'const Immutable' as 'this' // argument discards qualifiers }
This was not the full truth. Sometimes you have to distinguish between the logical and the physical constness of an object. Sounds strange, right?
Physical constness: Your object is declared const
and cannot, therefore, be changed. Its representation in memory is fixed.
Logical constness: Your object is declared const
but could be changed. Its logical value is fixed, but its representation in memory may change at run time.
Physical constness is quite easy to comprehend, but logical constness is more subtle. Let me modify the previous example a bit. Assume I want to change the attribute val
in a const
member function.
// mutable.cpp #include <iostream> struct Immutable { mutable int val{12}; // (1) void canNotModify() const { val = 13; } }; int main() { std::cout << '\n'; const Immutable immu; std::cout << "val: " << immu.val << '\n'; immu.canNotModify(); // (2) std::cout << "val: " << immu.val << '\n'; std::cout << '\n'; }
The specifier mutable
(1) made the magic possible. The const
object can, therefore, invoke the const
member function (2), which modifies val
. See Figure 12.1.
Figure 12.1 A mutable
variable
Typically, a mutex used in a class member variable is mutable
. Imagine your class has a read operation, which should be const
. Because you use the data of the class concurrently, you have to protect the read member function with a mutex. So the class gets a mutex, and you lock the mutex in the read operation. Now you have an issue. Your read member function cannot be const
because of the locking of the mutex. The solution is to declare the mutex as mutable
.
Here is a sketch of the presented use case. Without mutable
, this code would not work.
struct Immutable { mutable std::mutex m; int read() const { std::lock_guard<std::mutex> lck(m); // critical section ... } };
If you pass pointers or references to const
to a function, the intention of the function is obvious. The pointed to or referenced object cannot be modified. This observation matches the previous rule covered in the section Parameter Passing: In and Out in Chapter 4.
void getCString(const char* cStr); void getCppString(const std::string& cppStr);
Are both declarations equivalent? No! In the case of the function getCString
, the pointer could be a null pointer. This means you have to check it before its usage: if (cStr) ....
.
But there is even more. The pointer and the pointee can be const
.
const char* cStr:
cStr
points to a char
that is const;
the pointee cannot be modified but the pointer can.
char* const cStr:
cStr
is a const
pointer; the pointer cannot be modified but the pointee can.
const char* const cStr:
cStr
is a const pointer to a char
that is const
; neither the pointer nor the pointee can be modified.
Too complicated? Read the expressions from right to left or use a reference to const
.
Use |
If you want to share a variable immutable
between threads and this variable is declared as const
, you are done. You can use const
variables without synchronization, and you get the most performance out of your machine. The reason is quite simple. To get a data race, you need to have a mutable, shared state. I already wrote about data races in the section addressing concurrency and parallelism: “CP.2: Avoid data races.”
There is an additional problem to solve when using immutable and shared data in a concurrent environment. You have to initialize the shared variable in a thread-safe way. I have at least four ideas in mind.
Initialize the shared variable before you start a thread.
Use the function std::call_once
in combination with the flag std::once_flag
.
Use a static
variable with block scope.
Use a constexpr
variable.
In the rule “CP.3: Minimize explicit sharing of writable data,” I addressed these challenges.
constexpr
Use |
constexpr
values give you better performance, are evaluated at compile time, and are never subject to data races. You must initialize a constexpr
value constexprValue
at compile time:
constexpr double constexprValue = constexprFunction(2);
A constexpr
function constexprFunction
can be executed at compile time. There is no state at compile time. A constexpr
function, when executed at compile time, is pure. Pure functions have many advantages:
The function call can be replaced by the result.
The function can be performed on a different thread.
A function call can be reordered.
The function can easily be refactored or be tested in isolation.
Read more details about the benefits of constexpr
functions in previous rules for functions: