On 12-Jun-17 1:40 AM, Adam Badura wrote:
> [snip]
>>> PS 2 Why are you using "const" for non-reference function arguments?
>>
>> Same reason as for using `const` for local variables: it constrains what
>> can change, so it helps with understanding and maintaining the code.
>>
>>
>>> Does it provide any benefit?
>>
>> Because you ask, I think it wouldn't help to discuss the merits and
>> disadvantages objectively: you have likely already dismissed that. So,
>> authority arguments it is. That's fallacious, of course, but I don't shy
>> away from a few fallacies here and there if they can do good, so:
>
> Well, actually I would be more than happy to know "the merits and
> disadvantages objectively". So I would be grateful if you could
> direct me to some sources or explain them yourself (although maybe a
> separate thread would be better for this?).
OK.
First lets differentiate between /interface/ and /implementation/.
A top-level `const` on a formal argument is never part of a function's
interface, it doesn't matter at all to a caller, and so the C++ rules
make the following two declarations exactly equivalent, denoting the
same function, with the same function type:
void foo( int );
void foo( int const );
In particular, a header file can contain the declaration (interface)
void foo( int );
… and an implementation can then contain e.g. (implementation)
void foo( int const x ) { gurgle_snap( x ); cout << x << endl; }
… which
• is an implementation of the former declaration,
• clearly outputs the value that's passed to it, because `gurgle_snap`
can't modify `x` because `x` is `const`.
The ease with which I can conclude something about the call of the to me
unknown function `gurgle_snap`, and thereby, about the effect of
subsequent code in the function, is, in my opinion, a clear and
objective advantage of `const` on the formal argument (don't mind the
apparent inconsistency between “in my opinion” and “objective”, lest we
delve down into an Hofstadteresque infinite regress on the meaning).
I.e. it saves time for maintenance. ~80% of all coding is maintenance.
It's the same advantage as for `const` on a local variable.
And this advantage is not likely to be there if one doesn't apply
`const` by default, as one's general coding habit.
If `foo` is provided via a header-only module, then the `const` will in
practice appear in the declaration of the interface. For a potential
user of `foo` that's redundant wordage, just plain verbosity. Happily
that's what we have tools for, to produce a more pure interface
specification. E.g. that's done in Eiffel, and I think also in Java.
Unfortunately, I'm not familiar with such a tool for C++, although if
one spends a lot of money on licenses of refactoring tools (Rational
Rose comes to mind, but that's from 15 years ago) there will probably be
something like that.
So, `const` on value arguments /can/ have a slightly negative effect for
a header only module, by increasing verbosity, and hence increasing time
to read and understand the header.
Another such contextual disadvantage is that `const` prevents moving. If
a function stores an argument, or returns it, then it's desirable to be
able to move it. And this can make a huge difference.
For example, consider the following ¹contrived code to compute the
Collatz sequence recursively:
#include <vector>
#include <iostream>
#include <utility> // std::move
using namespace std;
auto is_odd( int const x ) -> bool { return x%2 == 1; }
auto concat( vector<int> v, int const x )
-> vector<int>
{
v.push_back( x );
return move( v );
}
auto collatz_aux( vector<int> numbers, int const n )
-> vector<int>
{
return (false? vector<int>{}
: n == 1? concat( move( numbers ), 1 )
: is_odd( n )? collatz_aux( concat( move( numbers ), n ),
3*n + 1 )
: collatz_aux( concat( move( numbers ), n ),
n/2 )
);
}
auto collatz( int const n )
-> vector<int>
{ return collatz_aux( {}, n ); }
auto main()
-> int
{
for( int const x : collatz( 42 ) )
{
cout << x << ' ';
}
cout << endl;
}
As given this has O(n) complexity, where n is the length of the final
sequence.
Change the `number` formal argument to `const`, and you prevent moving,
thus changing the /behavior/ to O(n^2) complexity.
That's not just some constant factor of inefficiency due to some extra
copying of data, it's one level up in the orders of inefficiency,
quadratic time. Of course, the compiler may still optimize that away.
But it would not be proper to rely on the compiler to figure that out.
So as a rule, where an argument is returned, or modified and returned,
as above, or copied to storage persisting beyond the function call, it's
generally a good idea to leave out the `const`.
But especially when the argument is returned without being modified
there is a conflict between these two ideals: being able to easily
reason about the effect and correctness of the code due to guaranteed
immutability of the argument, its `const`-ness, and supporting
efficiency, which requires some final pilfering of resources from the
argument, and hence that the argument is non-`const`.
Cheers & hth.,
- Alf
Notes:
¹ I'm indebted to Andrew Koenig for the Collatz example above, because I
invented it in response to an article he wrote about list-based
recursion, with a Collatz example, in Dr. Dobbs Journal.