In this article you will understand C++ in const

  • 2020-11-25 07:27:18
  • OfStack

At the highest level of abstraction, const does two things:

* 1 way to protect yourself (similar to private)

* An indication to the compiler that an object marked const is suitable for the data segment of the program. In other words, it belongs to read-only data (ES8en-ES9en).

Take a look at const in action with an example. In the first example, the entire example is covered with const:


void fun(int i, std::string const & str)
{
 i = 0;     //ok.
 str = "";    //error!
 int const n = 42;
 n = 2;     //error!
}

The second case applies only to statically initialized namespace-scope variables (also known as global variables):


int const pi = 3; //ROM-able
std::vector<int> const ivec = {/* ... */}; //Not ROM-able, might allocate.

Any write to a variable declared as const is shown as undefined behavior. This supports the location of the const global variable within ROM.

If a variable is defined in ROM, a write to it is likely to crash the program, depending on the platform. If a variable is not in ROM, a write to it will only change its value. The combination of these two situations is why the behavior of writing to the const variable is undefined rather than an error.

If you really need to rewrite the value of an const variable, you can do so by 'const_cast' :


void fun(int i, std::string const & str)
{
 i = 0; //ok.
 const_cast<std::string &>(str) = ""; //Also ok (maybe).
}

However, const_cast does not prevent you from never falling into the trap of undefined behavior when trying to write variables declared as const.


std::string str = "";
fun(0, str); // Ok.
std::string const const_str = "";
fun(0, const_str); // Undefined Behavior!!

Therefore, use const_cast only when you really need it, and only when you know how the underlying variable to be written is declared.

** So, exactly when and where to use it. * *

The answer is **Everywhere**. Declare each variable as const unless you know it will be written. Even more, add const wherever the compiler accepts it.


int foo(int arg)
{
 int const x = compute_intermediate_result(arg);
 int const y = compute_other_intermediate_result(x);
 return something_computed_from(x, y);
}

const from the optimizer's perspective

For optimization purposes, compilers generally cannot be optimized with 1 chirality.


int get_value(some_class const & x, int const at)
{
 int offset = compute_offset(at);
 return x[offset];
}

At this point, using const in these function arguments does not help the optimizer. const on x does not work because x is already passed by reference. There is no copy of x, and the compiler does not know whether x is declared as const. const on the parameter at does not help us because at is a copy and can be loaded into registers in any way.
If the compiler can see the declaration of an const object, it can sometimes be optimized with its 1 chirality.


std::vector<int> const vec = { 1, 2, 3 };

int main()
{
 // This may generate code that indexes into vec, or it may generate
 // code that loads an immediate 2.
 return vec[1];
}

If you want to ensure this optimization, you can use constexpr in c++11 or later. Variables declared using constexpr are compiled only for types that can be statically initialized, so if you compile it, you get an object of ES80en-ES81en.


constexpr std::array<int, 3> arr = { 1, 2, 3 };

int main()
{
 // This generates code that loads an immediate 2 on every
 // compiler I tried.
 return arr[1];
}

constexpr handles literal constant types only (literal types). These types are similar to those you might find in C. Any int or other integer value declared as const can be used like text 1.


void foo(int const arg)
{
 int const size = 2; 

 int array_0[2];  // Ok.
 int array_1[arg]; // Error! arg is a runtime value.
 int array_2[size]; // Ok.
}

Static const class member

Declaring a class member, static const, is much like declaring a global const variable.


int const global_size = 3;

struct my_struct
{
 static int const size = 2;
};

std::array<int, global_size> make_global_array()
{ return {}; }

std::array<int, my_struct::size> make_my_struct_array()
{ return {}; }

But since this is c++, there's a problem. If the defined static const integer value exceeds the bounds, it may not be used as a literal constant, as in the example under 1.


int const pi = 3; //ROM-able
std::vector<int> const ivec = {/* ... */}; //Not ROM-able, might allocate.
0

This is an error because the compiler does not know what value to use for my_class::size when parsing foo(). If you want to use static const integer values as you would use global const integer values 1, always declare them inline (inline).

conclusion

1. Use const to protect yourself from silly mistakes, and use it everywhere.

2. You can use const on globals that you want in ROM, but prefer constexpr.

3. Use const_cast sparingly, or not at all.

4. When you do use const_cast, be careful of the UB that might result, including crashes.

5. Use const globals and stack variables instead of macros for named values that are "as good as literals".

6. Define static const integral data members inline, if possible.


Related articles: