C++ Core Guidelines: Rules for Constants and Immutability

C++ Core Guidelines: Rules for Constants and Immutability

This is a cross-post from www.ModernesCpp.com.

Making objects or methods const has two benefits. First, the compiler will complain when you break the contract. Second, you tell the user of the interface that the function will not modify the arguments.

The C++ Core Guidelines has five rules to const, immutability, and . Here are they:

Before I dive into the rules, I have to mention one expression. When someone writes about const and immutability, you often hear the term const correctness. According to the C++ FAQ , it means:

  • What is const correctness??It means using the keyword?const?to prevent?const?objects from getting mutated.

Now, we know it. This post is about const correctness.

Con.1: By default, make objects immutable

Okay, this rule is quite easy. 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 will get what you deserve: 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
}        

?The error messages from the GCC are very convincing.

Con.2: By default, make member functions const

Declaring member functions as const has two benefits. An immutable object can only invoke const methods and const methods cannot modify the underlying object. Once more. Here is a short example which 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 distinguish between the logical and the physical constness of an object. Sounds strange. Right?

  • Physical constness: Your object is declared const and can, therefore, not be changed.
  • Logical constness: Your object is declared const but could be changed.

Okay, physical constness is quite easy to get but logical constness. Let me modify the previous example a little bit. Assume, I want to change the attribute in a const method.?

// mutable.cpp

#include <iostream>

struct Immutable{
    mutable int val{12};            // (1)void canNotModify() const {
        val = 13;
    }
};

int main(){
    
    std::cout << std::endl;
    
    const Immutable immu;
    std::cout << "val: " << immu.val << std::endl;
    immu.canNotModify();            // (2)std::cout << "val: " << immu.val << std::endl;
    
    std::cout << std::endl;
    
}        

The specifier mutable (1) made the magic possible. The const object can, therefore, invoke the const method (2) which modifies .

No alt text provided for this image

Here is a nice use-case for mutable. Imagine, your class has a read operation which should be const. Because you use the objects of the class concurrently you have to protect the read method with a mutex. So the class gets a mutex and you lock the mutex in the read operation. Now, you have a problem. Your read method 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 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
        ...
    }
};        

Con.3: By default, pass pointers and references to

Okay, this rule is quite obvious. If you pass pointers or references to to a function, the intention of the function is obvious. The pointed or referenced to object would not be modified.

void getCString(const char* cStr);
void getCppString(const std::string& cppStr);        

Are both declarations equivalent? Not one hundred per cent. In the case of the function getCString, the pointer could be a?null pointer. This means you have to check in the function vai if (cStr)

But there is more. The pointer and the pointed to object could be const.

Here are the variations:

  • const char* cStr: cStr points to a char that is const; the cannot be modified
  • char* const cStr:?cStr is a const pointer; the pointer cannot be modified
  • const char* const cStr: cStr is a const pointer to a char that is const; neither the pointer nor the could be modified

Too complicated? Read the expressions from right-to-left. Still too complicated? Use a reference to const.

I want to present the next two rules from the concurrency perspective. Let me do it together.

Con.4: Use const to define objects with values that do not change after construction and Con.5: Use for values that can be computed at compile time

If you want to share a variable immutable between threads and this variable is declared as const, you are done. You can use immutable without synchronisation and you get the most performance out of your machine. The reason is quite simple. To get a data race, you should have a mutable, shared state.

  • Data Race: At least two threads access a shared variable at the same time. At least one thread tries to modify it.

No alt text provided for this image

There is only one problem to solve. You have to initialise the shared variable in a thread-safe way. I have at least four ideas in my mind.

  1. Initialise the shared variable before you start a thread.
  2. Use the function std::call_once in combination with the flag std::once_flag.
  3. Use a static variable with block scope.
  4. Use a variable.

A lot of people oversee the variant 1 which is quite easy to do right. You can read more about the thread-safe initialisation of a variable in my previous post: Thread-safe Initialization of Data .

Rule Con.5 is about variant 4. When you declare a variable as double totallyConst = 5.5 is initialised at compile-time and, therefore, thread-safe.

That was not all to . The C++ core guidelines forgot to mention one import aspect of in concurrent environments. functions are sort of pure. Let's have a look at the

constexpr int gcd(int a, int b){
  while (b != 0){
    auto t= b;
    b= a % b;
    a= t;
  }
  return a;
}        

First, what does mean? And second, what does sort of pure mean?

A function can be executed at compile time. There is no state at compile time. When you use this function at runtime the function is not per se pure. Pure functions are functions that return always the same result when given the same arguments. Pure functions are like infinitely large tables from which you get your value. The guarantee that an expression returns always the same result when given the same arguments is called referential transparency .

Pure functions have a lot of advantages:

  1. The function call can be replaced by the result.
  2. The execution of pure functions can automatically be distributed to other threads.
  3. A function call can be reordered.
  4. They can easily be refactored or be tested in isolation.

In particular, the point 2 makes pure functions so precious in concurrent environments. The table shows the key points of pure functions.

No alt text provided for this image

I want to stress one point. functions are not per se pure. They are pure when executed at compile time.

What's next

That was is. I'm done with constness and immutability in the C++ core guidelines. In the next post, I start to write about the future of C++: templates and generic programming.

?

要查看或添加评论,请登录

Rainer Grimm的更多文章

  • My ALS Journey (17/n): Christmas Special

    My ALS Journey (17/n): Christmas Special

    Today, I have a special Christmas gift. My ALS Journey so far Make the Difference Let’s do something great together:…

  • std::execution

    std::execution

    std::execution, previously known as executors or Senders/Receivers, provides “a Standard C++ framework for managing…

    1 条评论
  • C++26 Core Language: Small Improvements

    C++26 Core Language: Small Improvements

    There are more small improvements in the C++26 language, which you should know. static_assert extension First, here’s…

  • My ALS Journey (16/n): Good Bye Training / Hello Mentoring

    My ALS Journey (16/n): Good Bye Training / Hello Mentoring

    In 2025, I will no longer offer C++ classes. Instead, I will only offer C++ mentoring in the future.

    1 条评论
  • Placeholders and Extended Character Set

    Placeholders and Extended Character Set

    Placeholders are a nice way to highlight variables that are no longer needed. Additionally, the character set of C++26…

    4 条评论
  • Contracts in C++26

    Contracts in C++26

    Contracts allow you to specify preconditions, postconditions, and invariants for functions. Contracts should already be…

    5 条评论
  • Mentoring as a Key to Success

    Mentoring as a Key to Success

    I know that we are going through very challenging times. Saving is the top priority.

  • Reflection in C++26: Determine the Layout

    Reflection in C++26: Determine the Layout

    Thanks to reflection, you can determine the layout of types. My examples are based on the reflection proposal P2996R5.

    5 条评论
  • My ALS Journey (15/n): A typical Day

    My ALS Journey (15/n): A typical Day

    You may wonder how my day looks. Let me compare a day before ALS with a current day.

    3 条评论
  • Reflection in C++26: Metafunctions for Enums and Classes

    Reflection in C++26: Metafunctions for Enums and Classes

    Today, I continue my journey through reflection in C++26 and play with enums and classes. The names of the…

    2 条评论

社区洞察

其他会员也浏览了