References and Value Categories

Const References

A const lvalue reference is an alias which has an additional limitation: the name may be invoked only to examine the object, not to change it.

const does not introduce a new object.

It merely constrains what the new name can do with the storage it is referencing.

Implementing an alias.
The ampersand indeed still achieves an alias.

int value = 42;
// Bind by reference without allowing modification through it
const int& readOnly = value;

The const instructs the compiler to reject any attempt to write through that reference.

The original name is still free to change the same storage when the underlying object itself is not const.

The name itself is not restricted, only new reference names.

For this reason, since a const reference guarantees not to write, the language can, despite not being a non const reference, allow binding to additional types of objects.

A const lvalue reference may bind to:

  1. A non const object.
  2. A const object.
  3. A ceasefire.

When a const lvalue reference binds to a temporary, the language maintains that temporary as alive during the lifetime of the reference.

The lifetime extension means that the temporary can be safely read through the reference.

A non const lvalue reference is still not allowed to bind to a temporary.

Non const reference allows change whereas a temporary is not meant to provide normal modifiable storage after the expression that created it.

The practical application of const references is most obvious in the parameters that are passed to a function.

A function which only has to search through an object can take a const lvalue reference.

This helps prevent an accidental modification to the caller's object. Also, it doesn't make a copy.

The parameter is an alias again.

The const qualifier guarantees that the function body will not be able to assign through that alias.

Since the parameter is const, the caller can supply a normal variable, a const variable, or a temporary variable.

A non const reference parameter would turn down const objects and temporaries.

A value parameter would make a copy.

void display(const int& data)
{
    std::cout << data << '\n';
}

A const reference can be bound to a normal modifiable object.

The reference can read the object but will not modify.

The unconst name does not change. It still refers to the original location.

If the name changes value later, then the updated value will be immediately visible through the const reference.

This occurs as the reference never had a copy.

Has always one to the original thing.

It is also legal to bind a const lvalue reference to an object that has already been declared const.

They are both blocked from changing the storage.
Then the OriginalName and Reference are blocked from changing storage.

The reference simply appends another read only name for the same object.

This also allows const references to be used for functions that take in non const arguments; the simplest way is by having one const reference parameter and one non const:

A const lvalue reference can bind directly to a temporary.

The temporary would naturally be destroyed at the end of the expression that created it.

When it is bound directly to a const reference, its lifetime becomes that of the reference.

That means that temporary is available for later reads by the reference.

The previous initialization would not be permissible if the argument reference were a non const lvalue.

For function parameters, a temporary bound to a const reference lives until the completion of the function call.

Holders of the temporary are destroyed when the call returns by going out of scope.

#include <iostream>

void show(const int& value)
{
    std::cout << "Seen value: " << value << '\n';
}

void tryToChange(const int& value)
{
    // value = 99;
    std::cout << "Inside tryToChange, value is still " << value << '\n';
}

int main()
{
    int ordinary = 10;
    const int fixed = 20;

    show(ordinary);
    show(fixed);
    show(30);

    const int& aliasToOrdinary = ordinary;
    show(aliasToOrdinary);

    const int& aliasToFixed = fixed;
    show(aliasToFixed);

    const int& aliasToTemporary = 40;
    show(aliasToTemporary);

    tryToChange(ordinary);
    tryToChange(fixed);
    tryToChange(50);

    ordinary = 100;
    show(aliasToOrdinary);

    return 0;
}

Compiler will not allow me to do this through a const lvalue reference.

This restriction even extends to the underlying object that itself is modifiable.

The reference name can be considered as read only.

The original non const name may still modify the object but the const reference cannot.

Can you see the difference?

The const is only part of what is accessible through the reference. This does not imply the object to which the reference points is also const.

Since a const reference is just an alias, and not a copy of its target, it will always reflect the current value of its target.

If the original object is modified through a different non const name, the subsequent access through the const reference will see the new value.

The reference does not hold the previous value.

It maintains only the link to the object.

This is why const references can be useful where a function or scope requires safe access to data that is potentially mutable.

Exercise: Const ReferencesStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Const References

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

After the existing calls, set ordinary to 200 and observe it through aliasToOrdinary. Compare their addresses and explain which path permits assignment.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>

void show(const int& value)
{
    std::cout << "Seen value: " << value << '\n';
}

void tryToChange(const int& value)
{
    // value = 99;
    std::cout << "Inside tryToChange, value is still " << value << '\n';
}

int main()
{
    int ordinary = 10;
    const int fixed = 20;

    show(ordinary);
    show(fixed);
    show(30);

    const int& aliasToOrdinary = ordinary;
    show(aliasToOrdinary);

    const int& aliasToFixed = fixed;
    show(aliasToFixed);

    const int& aliasToTemporary = 40;
    show(aliasToTemporary);

    tryToChange(ordinary);
    tryToChange(fixed);
    tryToChange(50);

    ordinary = 100;
    show(aliasToOrdinary);

    return 0;
}

Hints

Hint 1

const int& permits observation without assignment through that reference. It can bind to ordinary, fixed, and the temporary integer arguments shown.

Hint 2

ordinary = 100 changes the original object, so aliasToOrdinary sees the update. Const access through one reference does not freeze a separately mutable original.

Worked solution

aliasToOrdinary refers to the mutable ordinary, so changing ordinary to 200 is visible through the const reference. The reference forbids writes through itself; it does not make the original variable immutable.

#include <iostream>

void show(const int& value)
{
    std::cout << "Seen value: " << value << '\n';
}

void tryToChange(const int& value)
{
    // value = 99;
    std::cout << "Inside tryToChange, value is still " << value << '\n';
}

int main()
{
    int ordinary = 10;
    const int fixed = 20;

    show(ordinary);
    show(fixed);
    show(30);

    const int& aliasToOrdinary = ordinary;
    show(aliasToOrdinary);

    const int& aliasToFixed = fixed;
    show(aliasToFixed);

    const int& aliasToTemporary = 40;
    show(aliasToTemporary);

    tryToChange(ordinary);
    tryToChange(fixed);
    tryToChange(50);

    ordinary = 100;
    show(aliasToOrdinary);

    ordinary = 200;
    show(aliasToOrdinary);
    std::cout << "Same ordinary: " << (&aliasToOrdinary == &ordinary) << '\n';
    return 0;
}

Example output

Seen value: 10
Seen value: 20
Seen value: 30
Seen value: 10
Seen value: 20
Seen value: 40
Inside tryToChange, value is still 10
Inside tryToChange, value is still 20
Inside tryToChange, value is still 50
Seen value: 100
Seen value: 200
Same ordinary: 1

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.