Posts Obscure Fact About References
Post
Cancel

Obscure Fact About References

We have all been told that marking variables const is a good practice and that we should all use it whenever we can. This is all true but sometimes const is not doing what it should, or at least, what we think it should be doing. Today, I am going to talk about one of these strange cases where marking variables const could be a bit misleading…

Const References Behave As Any Other Const Type…

We would expect that applying const keyword to the reference has the same behavior or the same outcome as applying it to any other type of variable, that compilers should help us diagnose when we unintentionally modify it and possibly generate better code (though this is pretty rare). And this is really the case…

Following example:

1
2
3
4
int         foo{ 1 };
int const & bar{ foo };

bar = 2;

will obviously raise a compiler error saying something like:

error: cannot assign to variable ‘bar’ with const-qualified type ‘const int &’ bar = 2; ~~~ ^ note: variable ‘bar’ declared const here int const & bar{ foo };

Another obvious example:

1
2
3
4
5
auto p = std::make_pair(1, 2);

auto const & [ foo, bar ] = p;

foo = 3;

will cause pretty much the same compiler error as above.

At this stage, you might ask yourselves where’s the catch and what’s this post all about. Well, the next section will give you an answer to that…

Well, Not Always …

Let’s say we don’t want to use std::pair but instead we want to std::tie two variables together like:

1
2
3
4
5
6
7
8
int foo{ 0 };
int bar{ 1 };

auto const t = std::tie( foo, bar );
auto const & [ f, b ] = t;

f = 3;
std::cout << f << " " << b; // output: 3 1

What do you think the compiler error message would look like in the above example? We are trying to modify the value of something that should be a const reference, so logically, we expect the compiler to raise pretty much the same error message as with the other two examples above, right? Well… actually… This code compiles successfully, no error messages, no warnings. It simply outputs 3 1.

What happened here? Is it a compiler bug or …?

Pure Standard Behaviour

If we had taken a closer look at how std::tie works, we would have noticed that it returns std::tuple of references.

1
2
template< class... Types >
constexpr tuple< Types&... > tie( Types&... args ) noexcept;

Now, when we do auto const & [ f, b ] = t; reference itself will be const-qualified, not the value referenced by it. If we take a peek into the standard saying:

Cv-qualified references are ill-formed except when the cv-qualifiers are introduced through the use of a typedef-name or decltype-specifier, in which case the cv-qualifiers are ignored.

we will realize that, in this particular context, the const qualifier is actually being ignored.

KUDOS to those knowing this before reading this article.

To make the standard more clear, here are the examples. First, if we want to make a constant reference like:

1
2
int         i  { 0 };
int & const ref{ i };

the compiler will raise an error which is totally fine and logical because we can’t reassign references anyway. However, if we declare an alias for a reference type, then the cv-qualifiers are ignored, per standard:

1
2
3
4
5
6
7
int i{ 0 };

using int_ref = int &;
int_ref const & ref = i; // same behavior if we omit ’&’ here
++ref;

std::cout << ref << " " << i; // output: 1 1

Why is this so? Why are the cv-qualifiers ignored in this case?

Two words: generic programming. Without cv-collapsing rules generic programming life would be much harder because the following simple example would not compile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

template< typename T >
void fun( T const & ref )
{
    std::cout << "Ref Before: " << ref << std::endl; // output: 0
    ref++;
    std::cout << "Ref After: " << ref << std::endl;  // output: 1
}

int main()
{
    int   i  { 0 };
    int & ref{ i };

    fun<int &>( ref ); // calls fun( int & const & ref )

    return 0;
}

but, fortunately, cv-collapsing rules exist and this compiles (check on Godbolt).

Now, let’s check how to actually deal with those cases when const-qualifier is being ignored.

How To Deal With These Cases?

There is not much help in recognizing above strange cases other than being familiar with the standard. GCC and CLANG provide us with the -Wignored-qualifiers flag but, in some cases, it’s not of great help either.

In case of the CLANG compiler, the code snippet with specifying alias for the reference type results in a warning saying:

warning: ‘const’ qualifier on reference type ‘int_ref’ (aka ‘int &’) has no effect [-Wignored-qualifiers] int_ref const & ref = i; ^~~~~~

However, the example with structured bindings, we went through a few moments ago, compiles with no warning at all.

GCC, on the other hand, compiles both examples with no warnings. Check it yourself on Godbolt.

Not much help for us there 😕.

Conclusion

Throughout this article, we have seen a behavior that, I believe, not many of us have thought about before but it could lead us to some really obscure bugs in everyday life. Except for the fact that we should know the standard, compilers should help us diagnose these kinds of situations, and, effectively, help us put more focus on software logic than on implementation details such as this.

As always, let me know your thoughts in the comments section below… ⬇️👇

Trending Tags