1. 13
  1. 9

    Immutable by default prevents a lot of mistakes. Rust got this 100% correct. When I don’t see const on a variable in C++, I immediately believe that it’s going to change somewhere and I consider it a mistake if it doesn’t.

    1. 3

      afaik, erlang was there first…

    2. 4

      The compiler can (and will) optimize the code according to the const directives.

      Not really. Variables without the const annotation that aren’t modified will be optimized the same. Optimizers transform code to single-assignment form anyway.

      And for all indirect data (including variables whose address has “escaped”) C++ gives no guarantees about immutability, even when they’re const. const only means that you can’t modify it through this particular binding, not that it can’t be modified.

      There’s still value in trying to declare your intentions and using a more functional style, but don’t expect const to actually do anything in C or C++. It’s a broken design (e.g. const *const * is mutable).

      1. 3

        Not really. Variables without the const annotation that aren’t modified will be optimized the same. Optimizers transform code to single-assignment form anyway.

        There is one case where this is not true: If you have a global that has its address taken and which escapes from the compilation unit (or has sufficiently complex data flow that alias analysis gives up). Declaring the global (including statics) as const tells the compiler that any store to the global, irrespective of the layers of const casts and arbitrary bits of pointer arithmetic is UB. The compiler can then assume no stores will happen, even if it can’t track all of the places a pointer to the global ends up.

        You are correct for all other cases. This one is important though because it’s the one that made a big difference in Jason Turner’s talk (linked from the article).

        1. 3

          I don’t think it even needs to be a global. For example, take https://gcc.godbolt.org/z/Tds84bd7d. The compiler knows that i is const. When returning from the function it knows that it could not have changed so it can put 42 as an directly as an immediate in the assembly code (mov eax, 42).

          But if you change i to be non-const then it can no longer do this. Instead it has to perform a load from memory to get the current value of i before returning (mov eax, DWORD PTR [rsp+12]).

          If you’re wondering how i could possibly ever change, consider that takes_const_ptr might be implemented as *(int *)p = 0;. The standard says that this is completely fine if and only if you never actually pass a pointer to a const-qualified object to takes_const_ptr. So in the case where we pass a pointer to a non-const i, the compiler needs to assume that the value may change. If we’re passing a const i and takes_const_ptr tries to do this trick and modify the value then the result is UB so the compiler can assume that a const i never changes.

          1. 1

            Thanks. Interestingly, if you replace the int with a struct containing an int, gcc no longer performs that optimisation but clang does.

      2. 2

        Const is such a useful concept in C++ but nobody is seriously suggesting changing the default now. If I was put in charge of designing a language today I would break it down like this:

        • local variables - not const by default. They are variables - they vary.

        • function parameters - const by default (references and pointers could still point to non-const data). Maybe even always const, it simplifies ownership rules.

        • globals/statics - const by default

        • instance members - const by default except maybe in structs

        • instance methods - const by default

        1. 6

          local variables - not const by default. They are variables - they vary.

          FWIW there are a couple reasons why I disagree with this:

          A lot of times, variables don’t vary, they’re just used to hold intermediate computations:

          let foo = bar.baz.quux(stuff + other(73));
          let another = foo.something(foo.other, foo.yet_another);
          do_something_with(another, and_another);
          

          Clearly, nothing varies here.

          Second, in mathematics, they’re still called variables even though they don’t vary: something like x = f(x) means that x is defined as a fixed-point of x, rather than what it means in programming. (Haskell and similar languages take this approach.)

          1. 2

            Second, in mathematics, they’re still called variables even though they don’t vary

            I was being flippant but I still think that (in general) local variables should be mutable by default. The alternative is sometimes creating a bunch of named variables for little reason when all you wanted to do was combine them.

            As an aside, I find it fascinating that people educated in different disciplines have very different ideas on what makes clear code. I come from a computer science background with a history of low level C++ development but currently work with a lot of younger people from mathematical and physics training who are honestly a lot smarter than I am.

            They think nothing of storing data in unnamed tuples but are usually very good about const-correctness. Whereas I write much more verbose variable names but only use const where I think it is needed.

          2. 1

            local variables - not const by default

            I think a better solution here is to not have anything be the default: make the programmer be explicit about what they want (i.e. using const or something like var or mut, rather than just having a default if neither is specified). In my experience, this also has the added benefit of making the programmer more aware of when things should actually be constant, when maybe they wouldn’t have thought about it otherwise.

          3. 1

            in C++ the keyword const does not completely refer to immutability. For instance, you can use the keyword const in a function prototype to indicate you won’t modify it, but you can pass a mutable object as this parameter.

            I don’t know C++ enough, but doesn’t const makes object itself immutable, not only variable holding it? Unlike most languages, i.e. javascript, where const only makes variable constant, not its value. I.e. you can’t call non-const methods on this object, you can’t modify its fields. At least if it’s not pointer to object, seems that for pointers it’s complicated. I thought this works almost the same way as in Rust, where you can’t modify non-mut references.

            1. 7

              I don’t know C++ enough, but doesn’t const makes object itself immutable, not only variable holding it?

              It’s C++ so the answer to any question is ‘it’s more complicated than that’. The short answer is that const reference in C++ cannot be used to modify the object, except when it can.

              The fact that the this parameter in C++ is implicit makes this a bit difficult to understand. Consider this in C++:

              struct Foo
              {
                 void doAThing();
              };
              

              This is really a way of writing something like:

              void doAThing(Foo *this);
              

              Note that this is not const-qualified and so you cannot implicitly cast from a const Foo* to a Foo*. Because this is implicit, C++ doesn’t let you put qualifiers on it, so you need to write them on the method instead:

              struct Foo
              {
                 void doAThing() const;
              };
              

              This is equivalent to:

              void doAThing(const Foo *this);
              

              Now this works with the same overload resolution rules as the rest of C++: You can call this method with a const Foo* or a Foo*, because const on a parameter just means that the method promises not to mutate the object via this reference. There are three important corner cases here. First, consider a method like this:

              struct Foo
              {
                 void doAThing(Foo *other) const;
              };
              

              You can call this like this:

              Foo f;
              const Foo *g = &f;
              g->doAThing(&f);
              

              Now the method has two references to f. It can mutate the object through one but not the other. The second problem comes from the fact that const is advisory and you can cast it away. This means that it’s possible to write things in C++ like this:

              struct Foo
              {
                 void doAThing();
                 void doAThing() const
                 {
                   const_cast<Foo*>(this)->doAThing();
                 }
              };
              

              The const method forwards to the non-const one, which can mutate the class (well, not this one because it has no state, but the same is valid in a real thing). The second variant of this is the keyword mutable. This is intended to allow C++ programmers to write logically immutable objects that have internal mutability. Here’s a trivial example:

              struct Foo
              {
                 mutable int x = 0;
                 void doAThing() const
                 {
                   x++;
                 }
              };
              

              Now you can call doAThing with a const pointer but it will mutate the object. This is intended for things like internal caches. For example, clang’s AST needs to convert from C++ types to LLVM types. This is expensive to compute, so it’s done lazily. You pass around const references to the thing that does the transformation. Internally, it has a mutable field that caches prior conversions.

              Finally, const does not do viewpoint adaptation, so just because you have a const pointer to an object does not make const transitive. This is therefore completely valid:

              struct Bar
              {
                  int x;
              };
              struct Foo
              {
                Bar *b;
                void doAThing() const
                {
                  b->x++;
                }
              };
              

              You can call this const method and it doesn’t modify any fields of the object, but it does modify an object that a field points to, which means it is logically modifying the state of the object.

              All of this adds up to the fact that compilers can do basically nothing in terms of optimisation with const. The case referenced from the talk was of a global. Globals are more interesting because const for a global really does mean immutability, it will end up in the read-only data section of the binary and every copy of the program / library running will share the same physical memory pages, mapped read-only[1]. This is not necessarily deep immutability: a const global can contain pointers to non-const globals and those can be mutated.

              In the specific example, that global was passed by reference and so determining that nothing mutated it required some inter-procedural alias analysis, which apparently was slightly deeper than the compiler could manage. If Jason had passed the sprite arrays as template parameters, rather than as pointers, he probably wouldn’t have needed const to get to the same output. For example, consider this toy example:

              namespace 
              {
                int fib[] = {1, 1, 2, 3, 5};
              }
              
              int f(int x)
              {
                  return fib[x];
              }
              

              The anonymous namespace means that nothing outside of this compilation unit can write to fib. The compiler can inspect every reference to it and trivially determine that nothing writes to it. It will then make fib immutable. Compiled with clang, I get this:

                      .type   _ZN12_GLOBAL__N_13fibE,@object  # @(anonymous namespace)::fib
                      .section        .rodata,"a",@progbits
                      .p2align        4
              _ZN12_GLOBAL__N_13fibE:     # (anonymous namespace)::fib
                      .long   1                               # 0x1
                      .long   1                               # 0x1
                      .long   2                               # 0x2
                      .long   3                               # 0x3
                      .long   5                               # 0x5
                      .size   _ZN12_GLOBAL__N_13fibE, 20
              

              Note the .section .rodata bit: this says that the global is in the read-only data section, so it is immutable. That doesn’t make much difference, but the fact that the compiler could do this transform means that all other optimisations can depend on fib not being modified.

              Explicitly marking the global as const means that the compiler doesn’t need to do that analysis, it can always assume that the global is immutable because it’s UB to mutate a const object (and a compiler is free to assume UB doesn’t happen. You could pass a pointer to the global to another compilation unit that cast away the const and tried to mutate it, and on a typical OS that would then cause a trap. Remember this example the next time someone says compilers shouldn’t use UB for optimisations: if C/C++ compilers didn’t depend on UB for optimisation then they couldn’t do constant propagation from global constants without whole-program alias analysis.

              For anything else, the guarantees that const provides are so weak that they’re useless. Generally, the compiler can either see all accesses to an object (in which case it can infer whether it’s mutated and get more accurate information than const) or it can’t see all accesses to an object (and so must assume that one of them may cast away const and mutate the object).

              [1] On systems with MMUs and sometimes it needs to contain so may actually be mutable unless you’ve linked with relro support.

              1. 1

                No, you might have D in mind where const is transitive.

              2. 1

                The use of const here really confused me, even after reading the first section where it was walked back I had to keep reminding myself. This is partly because I use C# rather than C++ on a daily basis and in C# const means compile time constant. Having a program where all variables are compile time constant is nonsense. Also is the word ‘variable’ even correct if the field is not able to change?

                I understand these are semantic nit picks and not really relevant to the main point of the article but I think it is worth discussing how we understand and perceive language.