54

After much googling I've found a lot about marking functions and their parameters as const, but no guide on marking variables as const.

Here's a really simple example:

#include <string>
#include <iostream>

void example(const std::string& x) {
  size_t length = x.length();
  for (size_t i = 0; i < length; ++i) {
    std::cout << x.at(i) << std::endl;
  }
}

int main() {
  example("hello");
}

Why not make

size_t length = x.length();

const like

const size_t length = x.length();

by convention?

I know such a small, simple example really doesn't show any huge benefit to this, but it seems like it'd be helpful in a larger codebase where you might accidentally mutate a variable you shouldn't have mutated.

Despite that benefit, I don't really see it used that much (in the C++ codebases I've seen) or mentioned nearly as much as making functions and their parameters const.

Is there some downside to doing this other than having to type 5 extra characters? I haven't found much on the topic, and I don't want to shoot myself in the foot if it's a problem having so many consts.

Wolf
  • 9,679
  • 7
  • 62
  • 108
m0meni
  • 16,006
  • 16
  • 82
  • 141
  • 8
    This is an interesting question, but I'm concerned it might be too subjective for this site. (Personally, I say go for it!) – templatetypedef Aug 12 '16 at 19:23
  • I tend to code like this, but remember that this _might_ become stack-intensive (i.e., you don't reuse temporaries then). – lorro Aug 12 '16 at 19:26
  • 4
    Making variables which do not change constant is a good habit to improve readability of code. However, people get lazy too soon. –  Aug 12 '16 at 19:27
  • 14
    "*const is your friend: Immutable values are easier to understand, track, and reason about, so prefer constants over variables wherever it is sensible and make const your default choice when you define a value* " -- "C++ Coding Standards", Herb Sutter and Andrei Alexandrescu. – RHertel Aug 12 '16 at 19:29
  • 2
    "The reason that const works in C++ is because you can cast it away. If you couldn't cast it away, then your world would suck." - Anders Hejlsberg – Mason Wheeler Aug 12 '16 at 20:26
  • SO is funny. 2 weeks ago I asked a question why the modern C++ compiler do not use the highest warning level and the question got closed and down voted... – Peter VARGA Aug 12 '16 at 23:10
  • I think the main downsides are that you have to remember to write `const` everywhere, and it clutters your code when you read it later. – user253751 Aug 13 '16 at 00:38
  • You can not make functions const. That is only possible for member fuctions. Usually you do not make parameter const. It is only done for pointer and references because you otherwise could change values outside the function. –  Aug 13 '16 at 09:46
  • good question, because it focuses on **variables** and therefore how to structure code... – Wolf Jul 28 '17 at 11:25
  • Absolutely: it litters your code and limits your implementations and is hard to maintain and is a bad practice. It's like crying wolf: eventually you become numb to seeing const everywhere and start ignoring it because most of the time it's an insignificant usage of it without real value or meaning. Related: [Use of 'const' for function parameters](https://stackoverflow.com/questions/117293/use-of-const-for-function-parameters/60823004#60823004). – Gabriel Staples Apr 01 '20 at 08:56

6 Answers6

44

There are no downsides to marking variables you don't modify const.

There are some up-sides though: the compiler will help you diagnose when you unintentionally modify a variable you shouldn't/didn't mean to and the compiler may (although due to the language having const_cast and mutable this is rare) generate better code.

So, I'd advise; use const where you can. There are no downsides and your compiler can potentially help you spot bugs. No reason not to (except for a bit of extra typing).

Note that this extends to member functions as well. Make them const when you can - it lets them be used in more contexts and helps users reason about the code ("calling this function won't modify the object" is valuable information).

GJK
  • 37,023
  • 8
  • 55
  • 74
Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70
  • Note that, when some temp var is const, another programmer usually won't reuse it (removing const) but introduce a new const. _Usually_ that's acceptable (and even the right thing), but it might become too stack-intensive for e.g. deeply recursive functions. – lorro Aug 12 '16 at 19:28
  • 21
    Over verbosity is a downside. It can definitely make the code less readable and harder to maintain. In OP's code piece, for example, making *length* *const* doesn't add real value. Yes, it will protect the variable from unintended modification. On the other hand, if it's modified, the code probably has much deeper problems than accidental typo. That is in contrast to the *x* which is part of the API and making it *const* is an essential and important part of the contract compliance that the function requires from its users. – SomeWittyUsername Aug 12 '16 at 19:45
  • 19
    @SomeWittyUsername Never underestimate the ability of some junior maintenance programmer refactoring your code in the future and not paying attention to the fact that some variable was not intended to be modified, to accidentally introduce bugs - using `const` wherever you can helps stopping those people introducing bugs. I've seen it happen in real life more times than I wish I had.. – Jesper Juhl Aug 12 '16 at 19:50
  • 4
    True. But such errors are pretty rare and should be pretty easily discovered. On the other side the impact of "consting" is overall on the entire code base and on all the people working on it.. To emphasize - I'm referring mostly to the trivial cases such as the piece of code here – SomeWittyUsername Aug 12 '16 at 19:51
  • 3
    I don't think `const_cast` hinders optimisation that much : if the *value* has been declared `const`, trying to cast it away is UB. – Quentin Aug 12 '16 at 20:14
  • 6
    On the subject of "no downsides", if the variable has a class type and at some point in the future you decide to `std::move` from it, `const` will cause the copy constructor to be silently invoked instead of the move constructor if these constructors are declared in the usual way - taking `const A&` and `A&&`, respectively. – bogdan Aug 12 '16 at 20:37
  • To reinforce this: some new languages (I'm thinking of Swift and Scala) have marking unchanged variables "constant" a convention. With Swift you have `let` and with Scala you have `val`, and both of these are like the C++ code `const auto` – Doc Aug 13 '16 at 00:10
  • 2
    @bogdan If you've actively marked a variable as const, you should understand that it means the object is non-mutable, regardless of the method you use to change it, either explicit or implicit (such as via move semantics). In this case it's a bug rather than downside, IMHO. – SomeWittyUsername Aug 13 '16 at 08:16
  • @SomeWittyUsername Of course it's a bug. The downside is that in this case `const` will cause a *silent* bug, rather than a diagnostic. It's basically a (corner) case where `const` hinders rather than helps. – bogdan Aug 13 '16 at 09:49
  • 1
    In modern C++, with move semantics, there are a few situations where declaring a local variable `const` that you then return, will prevent the compiler from generating an automatic move. So be aware of that - clang-tidy has a check to catch those situations, which is helpful. – Jesper Juhl Oct 21 '21 at 19:51
22

I can think of at least two downsides:

  • verbosity: more words, more symbols to process, ...
  • inertia: if you need to modify it, you'll have to go and remove this const

and both are worth it.


Verbosity is an often heard argument against explicitness, however people often mistake reading speed with understanding speed. There is a balance to be found between verbosity and explicitness, certainly, too verbose may drown out useful information but too implicit/terse may not present information that has to be reconstructed/inferred/deduced/.

Personally, I use a strongly typed statically checked language so that the compiler picks out my mistake as early as possible; annotating with const is both giving information to the reader and the compiler. I judge it worth the extra 6 symbols.

As for inertia, removing const is probably only a small cost of the change... and it repays itself by forcing you to go through all the places where it's used and review the surrounding code to ensure it's actually alright to remove this const. Suddenly modifying a particular piece of data in a code path where it previously was immutable requires ensuring that no part of the code path (or its callers) accidentally relied on this immutability.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 2
    As the code changes, it's far easier to remove `const` later, if it turns out that you didn't need it, than it is to add it if it turns out you really should have been clear about the constness. In my mind, the `const` on a simple declaration is a small cost to pay for the information it conveys to the reader. When `const` is there, I know that the temporary was created just to give a meaningful name to an expression and not because it's a property that's going to change over time and perhaps have to abide by some non-obvious invariants. – Adrian McCarthy Aug 12 '16 at 21:45
  • 1
    @AdrianMcCarthy: Totally agree. – Matthieu M. Aug 13 '16 at 04:17
  • I think the inertia argument is important. While everyone says "its easy to remove the `const`," and they're right, you do run the risk of making it easier to do other solutions, like copying the value into a new variable. I wouldn't do it when deadlines are nice, but in that last 2 hours before a release when the code still isn't working, little hacks like that are surprisingly easy to make. In fact, just today I had to do that to an argument (not a local). The argument was const in the interface, but it didn't need to be. I didn't have time to fix the interface, so copy it is! – Cort Ammon Aug 13 '16 at 04:30
  • 1
    @CortAmmon: If you can afford the copy performance-wise, I would argue that copying is better because any change to the local is... local. No need to worry about far-ranging effects or anything. From a maintenance perspective, this guarantee of being side-effect free wrt. this argument is a nice property. Also, specifically to your anecdote, if it had not been annotated `const` and you had modified it... how confident would you be that you didn't accidentally break something? If you don't have time to check where it's used to guarantee they're not relying on the argument being immutable... – Matthieu M. Aug 13 '16 at 15:42
  • @MatthieuM. True, if all code was written perfectly, we'd only write perfect code =) The issue however, is that now the code still has inertia, and it is less perfect than before. Now a second tweak could create even more complexity that wasn't needed. If you have an unlimited refactor budget, this doesn't matter. However, when you're balancing refactoring against the other budgetary pressures, it's easy to get into situations where there isn't quite enough energy to fix the code, so instead we make it worse. I hate code that develops inertia because it has inertia. – Cort Ammon Aug 13 '16 at 15:51
  • I've been given the privileged of refactoring entire modules, and cutting the SLOC count down by 90% because almost all of the code in that module was hacks working around the inertia caused by the previous hack. It's very easy to double down on a bad assumption, rather than fixing it. – Cort Ammon Aug 13 '16 at 15:54
8

Instead of this ¹non-standard code:

#import <string>
#import <iostream>

void example(const std::string& x) {
  size_t length = x.length();
  for (size_t i = 0; i < length; ++i) {
    std::cout << x.at(i) << std::endl;
  }
}

int main() {
  example("hello");
}

… I'd write this:

#include <string>
#include <iostream>
using namespace std;

void example( string const& s )
{
    for( char const ch : s )
    {
        cout << ch << '\n';
    }
}

auto main()
    -> int
{ example( "hello" ); }

The main place I could add const, relative to the original code, was for the ch variable in the loop. I think that's nice. const is generally desirable because it reduces the possible code actions one has to consider, and range based loops let you have more const.

The main drawback of using const for most things, is when you have to relate to C APIs.

Then one just has to make some gut feeling decisions about whether to copy data, or trust in the documentation and use a const_cast.


        Addendum 1:
Do note that const on a return type prevents move semantics. As far as I know this was first noted by Andrei Alexandrescu in his Mojo (C++03 move semantics) article in Dr Dobbs Journal:

[A] const temporary looks like an oxymoron, a contradiction in terms. Seen from a practical perspective, const temporaries force copying at destination.

So, this is one place where one should not use const.

Sorry that I forgot to mention this originally; I was reminded by user bogdan's comment on another answer.


        Addendum 2:
In the same vein (to support move semantics), if the last thing done with a formal argument is to store a copy somewhere, then instead of passing by reference to const it can be better to use a non-const argument passed by value, because it can be simply moved from.

I.e., instead of

string stored_value;

void foo( string const& s )
{
    some_action( s );
    stored_value = s;
}

… or the redundancy of optimized

string stored_value;

void foo( string const& s )
{
    some_action( s );
    stored_value = s;
}

void foo( string&& s )
{
    some_action( s );
    stored_value = move( s );
}

… consider just writing

string stored_value;

void foo( string s )
{
    some_action( s );
    stored_value = move( s );
}

It can be slightly less efficient for the case of lvalue actual argument, it discards the advantages of const (constraints on what the code could possibly do), and it breaks a uniform convention of using const wherever possible, but it doesn't perform badly in any situation (which is the main goal, to avoid that) and it's smaller and possibly more clear code.


Notes:
¹ Standard C++ does not have an #import directive. Also, those headers, when properly included, are not guaranteed to define size_t in the global namespace.

Community
  • 1
  • 1
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • 3
    Generally I'd make the variable in a range-for-loop a `const &`. For some types it doesn't matter, for some types it avoids an expensive copy. So unless you explicitly *want/need* a cooy, `const &` seems the sane default.. – Jesper Juhl Aug 12 '16 at 19:38
  • Quick question...according to this [SO Post](http://stackoverflow.com/a/162615/3772221), wouldn't you be able to modify that string? Reading `string const&` right to left it's a constant reference to a mutable string. Sorry to bikeshed...just curious haha. – m0meni Aug 12 '16 at 19:41
  • 1
    @AR7 const applies to the left unless there's no left, in which case it applies to the right. `const string&` is the same as `string const&`. +References are always const (unlike pointers where it may make sense to say `something* const` == an immutable pointer to something mutable). – Petr Skocik Aug 12 '16 at 19:43
  • 1
    Making *const* of a variable that represents a **copy** is pretty meaningless in most cases. +1 to @JesperJuhl approach – SomeWittyUsername Aug 12 '16 at 19:49
  • @SomeWittyUsername I don't see why? You are going to use that copy and you don't want it to be modified before it gets used. – Galik Aug 12 '16 at 20:04
  • 2
    @JesperJuhl But in this case a `const&` would likely slow the loop down. Copying a single `char` is likely to be rather faster than dereferencing. – Galik Aug 12 '16 at 20:06
  • @Galik Consider the scope. The actual data that you care about is safe (in *s* in this example). – SomeWittyUsername Aug 12 '16 at 20:08
  • @SomeWittyUsername Only in this trivial example. If it were a math calculation with a bunch of variables you want to make sure the non-changing ones don't change. – Galik Aug 12 '16 at 20:10
  • @Galik Maybe so. On the other hand, in such case code refactoring might be a better approach. – SomeWittyUsername Aug 12 '16 at 20:16
  • 15
    Is no one going to point out the pointlessness of a trailing return type on (what should be by convention) `int main()` or the use of `using namespace std` in the "preferred" method? – jonspaceharper Aug 12 '16 at 20:19
  • @SomeWittyUsername Code refactoring should be done because the code needs refactoring not to avoid using `const`. What I mean is that code refactoring is orthogonal to whether or not a variable that is not supposed to change should be marked `const`. – Galik Aug 12 '16 at 20:22
  • @Galik I meant that if the code is written in a way that local temporary variables should be protected from modifying, such code should probably be refactored in a first place, regardless of consting. – SomeWittyUsername Aug 12 '16 at 20:42
  • @SomeWittyUsername I suppose I just don't know of a way to write code such that local variables that should not be modified won't benefit from being marked `const`. – Galik Aug 12 '16 at 20:47
  • @Galik Rewrite it with several functions – SomeWittyUsername Aug 12 '16 at 20:52
  • I updated the answer after being reminded of return values by a comment elsewhere. Is there more I've forgotten to mention? – Cheers and hth. - Alf Aug 12 '16 at 21:01
  • Where's the standard violation in the first snippet? – user2357112 Aug 12 '16 at 21:06
  • 1
    @user2357112: E.g., standard C++ does not have an `#import` directive. Also, those headers, when properly included, are not guaranteed to define `size_t` in the global namespace. – Cheers and hth. - Alf Aug 12 '16 at 21:08
  • @JonHarper: Good conventions are not pointless; you could have asked instead of asserted (and misleading others). `using namespace std;` is perfectly fine in a short example or beginner's program; ditto. The quotes around "preferred" in your comment seems to indicate that you're happy with the non-standard original code, that's surprising. – Cheers and hth. - Alf Aug 12 '16 at 21:21
  • 4
    I did not assert that good conventions are pointless, I asserted that your *not following* a good convention is pointless (at best, counterproductive at worst). The UNS argument is a dead horse; I'll leave our disagreement where it is. The "preferred" is a disambiguation to simplify what I could have called "the code you wrote". – jonspaceharper Aug 12 '16 at 21:35
  • @JonHarper: Sounds like social issues to me. Good luck. – Cheers and hth. - Alf Aug 12 '16 at 23:39
  • 8
    "auto main() -> int" - really? – user253751 Aug 13 '16 at 00:39
  • 1
    @immibis: It's the modern syntax, from C++11 (we're now at C++14, with C++17 coming up). It's better in most ways than the old C++03 syntax, and some functions can't be expressed in the old syntax. You have some serious catching up to do, but it'll be fun. :-) – Cheers and hth. - Alf Aug 13 '16 at 09:08
  • 2
    @Cheersandhth.-Alf I know what the syntax means, there's just no reason to use it here. – user253751 Aug 13 '16 at 09:35
  • @immibis: I use it as a single convention, and it is a good convention. As I see it there is no reason to use both syntaxes except for legacy code. If you think about it, there's something silly about mingling two different syntaxes, where one of them is so limited it can't be used in all cases, and where also that syntax has a bunch of problems. I decided long ago to not mislead newbies by using that no-advantage-many-disadvantages silliness in SO answers. What to do in a project with old project guidelines, is something else. Arguments to the contrary have all so far been social. Conformism. – Cheers and hth. - Alf Aug 13 '16 at 09:40
  • 1
    Though note that main is special case anyway, its specialness defined in the standard, and pretty much dictated by legacy reasons. So having different syntax for main can be seen as intentionally indicating it is special. – hyde Aug 14 '16 at 06:20
  • 1
    To follow up on @hyde, consider searching for "int main" and getting no results... – jonspaceharper Aug 14 '16 at 19:21
5

For the local variable size_t length in a short method like this it doesn't really matter. The downside of extra verbosity basically balances with the relative safety of avoiding typos accidentally modifying the length. Do whatever your local style guide, or your own gut feeling tells you.

For a longer or more complex method, it might be different. But then again, if you have so complex a method that it matters, maybe you should at least consider refactoring your code to simpler pieces... Anyway, if you read and understand the code, extra hint provided by explicit const is kinda irrelevant - nice but irrelevant.


Slightly related, though you didn't ask about it: For the reference parameter of your example method, you definitely do want const, because you might need to pass it a const string. Only if you want to disable passing const string (because you think you'll add code to modify it), you should omit const there.

hyde
  • 60,639
  • 21
  • 115
  • 176
3

I know such a small, simple example really doesn't show any huge benefit to this, but it seems like it'd be helpful in a larger codebase where you might accidentally mutate a variable you shouldn't have mutated.

The problem is that this basically never actually happens.

On the other hand, const is a disease that will spread through your codebase like a plague. As soon as you declare a const variable, all the things you need on it must be const, and so they must only call const functions, and it just never stops.

const isn't remotely worth the price you pay for it in the infinite majority of situations. There's only a couple of cases where const actually protects you (e.g. set keys), but even then, it's debatable if you'd have to be a total moron to try that in the first place, and probably not worth all the language rules and incessant code duplication and redundant metalogic.

const is a nice idea that might be nice in theory, but the practical realities are that const is a total waste of time and space. Burn it with fire.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • This was what I was looking for haha. Do you have any examples you've run into personally? – m0meni Aug 12 '16 at 21:05
  • 2
    Just use Rust, then you don't need to write `const`. ;-) – Cheers and hth. - Alf Aug 12 '16 at 21:07
  • 1
    I wanted to use a pointer as a key in an `unordered_map`, then call some functions on it which are all non-mutating, but then I had to mark them all as const, and ... – Puppy Aug 12 '16 at 21:33
  • `but the practical realities are that const is a total waste of time and space. Burn it with fire.` Hm, as of my experience at least, I encountered more bugs by far due to unintended modifications than I had issues due to inflated const-usage. It's right, const at least at interface level, is a design element and should be used like every design element and aspect: with reason. – Secundi Oct 07 '21 at 19:06
1

I agree with most of the answers given so far, on the other hand, some aspects are still missing.

When defining interfaces, the const keyword is your friend. But you should know that it's also somewhat limited and sometimes even egoistic - this is what I'd call its downsides.

Let's have another close look on the question:

Are there any downsides to marking all variables you don't modify const?`

If you observe that you don't modify something, you may say that it's effectively a constant. Also code analysis tools can detect this, even your compiler will already know it. But this observation should not trigger an add-const reflex in you.

Instead think about the variable itself, ask

  • Is it useful at all?
  • Is it also intended to be constant?

Sometimes an intermediate variable can simply be removed, sometimes a small rework (adding or removing a function) can be done to improve the code.

Adding the const keyword may harden your code against errors elsewhere, but also against changes at the declaration point.

Let me also add a word about member variables that don't change. If you decide to declare a member variable const, you have to initialize it in the initializer list of the constructor of the containing class, this extends the preconditions for constructing objects of this class.

So don't add const wherever your compiler allows for it. Do not be seduced to "petrify your code" ;-)

Wolf
  • 9,679
  • 7
  • 62
  • 108