36

Consider the following code:

#include <iostream>

void f(int const& a, int& b)
{
  b = a+1;
}

int main() {
  int c=2;
  f(c,c);
  std::cout << c << std::endl;
}
  • Function f takes two reference arguments: int const& a and int& b. Therefore, f is supposed not to modify a, but it can modify b, and indeed it does.
  • However, in main, I pass the same variable, referenced both by a and by b. As f modifies b, it also modifies a, which it supposedly shouldn't

This code compiles without any warning and prints 3. If we track each variable individually, it looks like const-correctness is respected: c is non-const, therefore it is perfectly fine to pass it as a const ref as a, and also as a non-const ref as b, and within the body of f we modify b, which is non-const, while not touching a, which is const. However, when c is employed both as a and as b, a is modified within the body of f, violating the assumption that a is const, even though no explicit const_cast was ever called.

I made this example as simple as I could, but one can easily think of not-so-obvious use cases (such as a const method acting on a non-const reference argument).

My question is:

  • Can we really say the above code is const-correct?
  • Is the above use case a known pattern? Is it considered bad practice?
  • Other than being confusing to a reader, can the above code be the source of technical problems? Such as undefined behavior, or the compiler performing wrong simplifying assumptions?
  • What if you used a return value instead of a reference parameter? Would `c = f(c);` seem strange? – Fred Larson Jun 15 '20 at 14:24
  • My guess: For `f()`, the `const` correctness is OK. It does what it promises: not modifying `a` and modifying `b`. The abuse happens in `main()` (using the same variable for `a` and `b`). What else you could do: checking in `f()` that `&a != &b` and/or remarking a resp. constraint in the doc. of `f()` (if necessary). – Scheff's Cat Jun 15 '20 at 14:25
  • @FredLarson that would be different. In my example, I have a `const` variable being modified `within` the function. After `b = a+1;`, `a` has changed its value. I could put the print inside `f` to make my concern more obvious – Gabriele Buondonno Jun 15 '20 at 14:27
  • 6
    I think the lesson is return values are clearer than output parameters. Prefer them. – Fred Larson Jun 15 '20 at 14:29
  • 1
    To answer your question: Yes. Yes. Yes, inferior to using a return value. Yes for the programmer; No for the compiler. No and No. – Eljay Jun 15 '20 at 14:34
  • @Scheff, no, I don't think it would change anything, the value of `b` on the right-hand-die would be simply 2, it would be used for the calculations, and the result woulb be `b=5`. I could have written `b=3` and my question would be the same, I only wrote `a+1` so that the compiler does not complain about an unused variable. – Gabriele Buondonno Jun 15 '20 at 14:37
  • You are completely correct. I became aware of this and considered something even more convoluted. Finally, I just deleted the comment in the hope that nobody has already noticed. (Mission failed.) ;-) – Scheff's Cat Jun 15 '20 at 14:44
  • Concerning the other comment and _checking that `&a != &b`_: That's something I already saw similar in overloaded assignment operators (like e.g. `if (this == &rhs) return *this; // early bail-out`) where self-assignment is considered as border case which is intended to be allowed but needs to be handled specifically. – Scheff's Cat Jun 15 '20 at 14:46

4 Answers4

32

However, in main, I pass the same variable, referenced both by a and by b. As f modifies b, it also modifies a, which it supposedly shouldn't

When f modifies what b refers to, it does not modify a. It modifies what a refers to, but that is okay since b is not const. It's only when you try to modify what a refers to by using a that you have issues.

Can we really say the above code is const-correct?

Yes. You do not modify a const variable.

Other than being confusing to a reader, can the above code be the source of technical problems? Such as undefined behavior, or the compiler performing wrong simplifying assumptions?

No, your code is legal and will produce the same results on all conforming compilers.


A constant reference parameter does not make the thing that it refers to const if it was not const to begin with. All it does it stop you from using the reference to modify the object. Another pointer or reference to that object can still mutate it, as long as it is not const itself.

Petter Friberg
  • 21,252
  • 9
  • 60
  • 109
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
8

Yes, the code is const-correct and, no, this particular code not have undefined behavior. What you wrote here is a simple case of reference aliasing, which boils down to pointer aliasing under the hood.

That being said, aliasing like this is generally undesirable, since it is, indeed, more complicated to reason about for both the programmer and the compiler. Also, this prevents certain optimizations, especially dealing with blocks of memory.

jvd
  • 764
  • 4
  • 14
  • 1
    Note that the optimizations you are talking about will be prevented, whether you actually do alias the pointers or not. Since the compiler has no way of knowing whether a function will be called with aliasing arguments, it always has to assume the worst case when generating code for the function. This is one of the prime performance advantages of Fortran (which bans pointer aliasing completely) over C and C++ to this day. – ComicSansMS Jun 16 '20 at 06:49
  • Unless the compiler does global analysis for aliases, which as far as I understand many modern compilers can do (even before different compilation units). – Hans Olsson Jun 16 '20 at 08:18
3

From the API perspective of

void f(int const& a, int& b), 

f promises not to modify anything via the a reference, thus respecting const correctness on a. It moreover informs the user that b, on the other hand, is very likely to be used to modify the object which it addresses; b would likely be documented as an [in, out] parameter, or just an [out] parameter, to f. If b is in fact never used to modify the object which it addresses, and moreover have no other design reason to be a non-const reference, that, on the other hand, could arguably be a (weaker) violation of const correctness by the implementer of f.

How a user makes uses or mis-uses this API is out of scope for f itself to directly worry about, particularly once its API design choice has been made. Any user-facing API should, however, arguably be designed as to minimize (given its design constraints) the risk of a user shooting him-/herself in the foot. E.g. in this case, a value semantics approach int f(int const& a) or int f(int copy_in_a) could be used to construct a different and harder-to-abuse interface towards the user.

dfrib
  • 70,367
  • 12
  • 127
  • 192
1

This is indeed a bad practice, because many programmers will (incorrectly) assume that the value of int const& a is indeed constant during the call. Consider

void f(int const& a, int& b)
{
  b = a+1;
  if(something) b = a+2;
}

It will be quite surprising to see b get the value of a+3, yet this is what can happen if a and b point to the same variable.

Dmitry Grigoryev
  • 3,156
  • 1
  • 25
  • 53