Search code examples
c++referenceconstantsparameter-passing

Can a function with a const-reference as its parameter change the underlying object?


A const reference to a non-const variable can be cast to a non-const reference and then the underlying object can be modified through the reference. Does it also hold for const-references in function declarations?

Namely, in the following code, is func() allowed to change 'a' provided 'a' is not originally const? Or would this lead to undefined behaviour?

void func(const int& arg);

int main()
{
   int a = 5; //non-const
   func(a);
   std::cout << a; //Can func modify 'a' through a const-cast and thus output ≠5?
}

I am asking this since this would prevent the compiler from doing an optimization as it is forced to look the value of 'a' up again after func is evaluated; especially if the definition of func is in another translation unit.


Solution

  • Yes, it can.

    Calling func will make a reference which has an "unnecessary" const:

    A pointer or reference to a cv-qualified type need not actually point or refer to a cv-qualified object, but it is treated as if it does; [...]

    - [dcl.type.cv] p3

    func can remove this "unnecessary" const with const_cast:

    void func(const int& arg)
    {
        int& evil = const_cast<int&>(arg); // OK so far
        evil = 123; // undefined behavior only if arg refers to a const object
    }
    

    In this case, func can modify a through evil, because a is not a const object. However, if a was actually const, then this would be undefined behavior:

    Any attempt to modify a const object during its lifetime results in undefined behavior.

    - [dcl.type.cv] p4

    In general, if a function is given a reference or pointer, it can simply const_cast or reinterpret_cast that reference to any type it wants. However, if the type of object that is accessed is not similar to that of the reference, this is undefined behavior in most cases. The access may be undefined, the const_cast or reinterpret_cast itself is fine.

    Impact on Compiler Optimizations

    Consider the following simplified example:

    void func(const int&); // "Black box" function.
                           // The compiler cannot prove that func isn't modifying
                           // its arguments, so it must assume that this happens.
    int main()
    {
       int a = 5;
       func(a);  // func may be modifying a.
       return a; // The compiler cannot assume that this is 'return 5'.
    }
    

    With optimizations, clang outputs:

    main:
            push    rax
            mov     dword ptr [rsp + 4], 5
            lea     rdi, [rsp + 4]
            call    func(int const&)@PLT
            mov     eax, dword ptr [rsp + 4]
            pop     rcx
            ret
    

    See live example at Compiler Explorer

    This code does suffer from the fact that func can modify a.

    • mov dword ptr [rsp + 4], 5 stores a on the stack
    • mov eax, dword ptr [rsp + 4] loads a from the stack after func was called

    If you write const int a = 5; instead, the assembly ends with mov eax, 5, and a is not spilled onto the stack, because it cannot be modified by func. This is more efficient.