When reviewing code, I apply the following rules:
Always use const
for function parameters passed by reference where the
function does not modify (or free) the data pointed to.
int find(const int *data, size_t size, int value);
Always use const
for constants that might otherwise be defined using a #define or an enum. The compiler can locate the data in read-only memory (ROM) as a result (although the linker is often a better tool for this purpose in embedded systems).
const double PI = 3.14;
Never use const in a function prototype for a parameter passed by
value. It has no meaning and is hence just 'noise'.
// don't add const to 'value' or 'size'
int find(const int *data, size_t size, int value);
Where appropriate, use const volatile
on locations that cannot be changed by the program but might still change. Hardware registers are the typical use case here, for example a status register that reflects a device state:
const volatile int32_t *DEVICE_STATUS = (int32_t*) 0x100;
Other uses are optional. For example, the parameters to a function within the function implementation can be marked as const.
// 'value' and 'size can be marked as const here
int find(const int *data, const size_t size, const int value)
{
... etc
or function return values or calculations that are obtained and then never change:
char *repeat_str(const char *str, size_t n)
{
const size_t len = strlen(str);
const size_t buf_size = 1 + (len * n);
char *buf = malloc(buf_size);
...
These uses of const
just indicate that you will not change the variable; they don't change how or where the variable is stored. The compiler can of course work out that a variable is not changed, but by adding const
you allow it to enforce that. This can help the reader and add some safety (although if your functions are big or
complicated enough that this makes a great difference, you arguably have other
problems). Edit - eg. a 200-line densely coded function with nested loops and many long
or similar variable names, knowing that certain variables never change might
ease understaning significantly. Such functions have been badly designed or
maintened.
Problems with const
. You will probably hear the term "const poisoning".
This occurs when adding const
to a function parameter causes 'constness' to
propagate.
Edit - const poisoning: for example in the function:
int function_a(char * str, int n)
{
...
function_b(str);
...
}
if we change str
to const
, we must then ensure that fuction_b
also takes
a const
. And so on if function_b
passes the str
on to function_c
,
etc. As you can imagine this could be painful if it propagates into many
separate files/modules. If it propagates into a function that cannot be
changed (eg a system library), then a cast becomes necessary. So sprinkling
const
around in existing code is perhaps asking for trouble. In new code
though, it is best to const
qualify consistently where appropriate.
The more insidious problem of const
is that it was not in the original
language. As an add-on it doesn't quite fit. For a start it has two meanings
(as in the rules above, meaning "I'm not going to change this" and "this cannot be modified"). But more than that, it can be dangerous. For example, compile and
run this code and (depending upon the compiler/options) it may well crash when
run:
const char str[] = "hello world\n";
char *s = strchr(str, '\n');
*s = '\0';
strchr
returns a char*
not a const char*
. As its call parameter is
const
it must cast the call parameter to char*
. And in this case that
casts away the real read-only storage property. Edit: - this applies generally to vars in read-only memory. By 'ROM', I mean not just physical ROM but any memory that is write-protected, as happens to the code section of programs run on a typical OS.
Many standard library functions behave in the same way, so beware: when you
have real constants (ie. stored in ROM) you must be very careful not to
lose their constness.