I'm going to kind of suggest a roundabout solution.
of course if Foo takes obj as value or const reference, we wont have
any issues.
string Foo (const myType & input); //this is fine
string Foo (myType input); // so is this
but we are not guaranteed this! the function signature could very well
be
string Foo (myType & input); //asking for trouble!
I think there's something more troublesome here. What we're not seeing is the documentation of this Foo
function: its interface comments, a meaningful name, etc.
The first thing to understand about this Foo
function before we even use it are the side effects it has. If we don't know what it's going to do with the arguments we pass in without a constness guarantee (which is only a weak guarantee as pointed out and becomes weaker the more const_casts
you introduce), then I would suggest that this might point to a breakdown in the way Foo
is documented, overloaded, or the way it is being used.
Whatever Foo
is actually called, whether it's rotate
, display
, clamp
, lerp
, paint
, flip
, info
, etc., it should be clear about its side effects, and they should not vary at a logical level between overloads. Interfaces should carry even firmer guarantees with respect to invariants than a named constant about what they will and will not do.
For example, if you have an interface design like this:
/// @return A flipped 's' (no side effects).
Something flip(Something s);
/// Flips 's' (one side effect).
void flip(Something& s);
... this is an extremely problem-inducing design: a tripwire for all developers who use it, a bug nest/hive, as the overloads vary disparately in terms of their side effects. A much less confusing design would be like this:
/// @return A flipped 's' (no side effects).
Something flipped(Something s);
/// Flips 's' (one side effect).
void flip(Something& s);
... one that doesn't overload flip
based on logical side effects.
If you ever encounter a design like this and it's outside of your control, I would suggest wrapping it to something more sane like introducing that flipped
function:
/// @return A flipped 's' (no side effects).
Something flip(Something s);
/// Flips 's' (one side effect).
void flip(Something& s);
/// @return A flipped 's' (no side effects).
Something flipped(Something s)
{
flip(s);
return s;
}
... and using that flipped
function instead where you clearly understand its side effects and what it's supposed to actually do and will continue doing independent of the mutability of the arguments you pass in. While this is more roundabout than introducing a const_cast
to invoke the right immutable overload of the function, it's plugging the source of confusion at the root rather than working around a very trippy design by forcing things to be passed with constness
.
constness
is best used as a defensive mechanism for potential changes that could occur in the future, not to kind of discover/enforce the proper behavior in the present. Of course you could approach it with the rationale of guaranteeing that Foo(obj)
won't trigger side effects in obj
in the future (presuming it doesn't in the present), but at an interface level, there shouldn't be instability with respect to side effects of this sort. If Foo(obj)
doesn't modify obj
today, then it definitely shouldn't tomorrow. At the very least, an interface should be stable in that regard.
Imagine a codebase where calling abs(x)
didn't leave you feeling 100% sure whether x
would be modified or not, or at least not in the future. That's not the time to reach for constness to solve this problem: the problem here would be totally at the interface/design level with respect to abs
. There shouldn't be mutable parameter overloads of abs
that produce side effects. There shouldn't ever be anything of this sort even 10 years down the line, and that should be a firm guarantee you can depend upon without forcing your arguments to abs
to be const
. You should be able to have a similar degree of confidence for any function you use provided it's even remotely stable.
So while there may be exceptions to the rule, I would suggest to check your interfaces, make sure they document things properly, aren't overloaded in a way that produces disparate logical side effects based on which overload you use, and are stable with respect to what they're documented to do.