Const Correctness in C VS C++

Const correctness in C vs C++

In addition to the differences you cite, and the library differences that
Steve Jessop mentions,

char* p1;
char const* const* p2 = &p1;

is legal in C++, but not in C. Historically, this is because C
originally allowed:

char* p1;
char const** p2 = &p1;

Shortly before the standard was adopted, someone realized that this
punched a hole in const safety (since *p2 can now be assigned a
char const*, which results in p1 being assigned a char const*); with
no real time to analyse the problem in depth, the C committee banned any
additional const other than top level const. (I.e. &p1 can be
assigned to a char ** or a char **const, but not to a char const**
nor a char const* const*.) The C++ committee did the further
analysis, realized that the problem was only present when a const
level was followed by a non-const level, and worked out the necessary
wording. (See §4.4/4 in the standard.)

Why is const-correctness specific to C++?

Well, it will have taken me 6 years to really understand, but now I can finally answer my own question.

The reason C++ has "const-correctness" and that Java, C#, etc. don't, is that C++ only supports value types, and these other languages only support or at least default to reference types.

Let's see how C#, a language that defaults to reference types, deals with immutability when value types are involved. Let's say you have a mutable value type, and another type that has a readonly field of that type:

struct Vector {
public int X { get; private set; }
public int Y { get; private set; }
public void Add(int x, int y) {
X += x;
Y += y;
}
}

class Foo {
readonly Vector _v;
public void Add(int x, int y) => _v.Add(x, y);
public override string ToString() => $"{_v.X} {_v.Y}";
}

void Main()
{
var f = new Foo();
f.Add(3, 4);
Console.WriteLine(f);
}

What should this code do?

  1. fail to compile
  2. print "3, 4"
  3. print "0, 0"

The answer is #3. C# tries to honor your "readonly" keyword by invoking the method Add on a throw-away copy of the object. That's weird, yes, but what other options does it have? If it invokes the method on the original Vector, the object will change, violating the "readonly"-ness of the field. If it fails to compile, then readonly value type members are pretty useless, because you can't invoke any methods on them, out of fear they might change the object.

If only we could label which methods are safe to call on readonly instances... Wait, that's exactly what const methods are in C++!

C# doesn't bother with const methods because we don't use value types that much in C#; we just avoid mutable value types (and declare them "evil", see 1, 2).

Also, reference types don't suffer from this problem, because when you mark a reference type variable as readonly, what's readonly is the reference, not the object itself. That's very easy for the compiler to enforce, it can mark any assignment as a compilation error except at initialization. If all you use is reference types and all your fields and variables are readonly, you get immutability everywhere at little syntactic cost. F# works entirely like this. Java avoids the issue by just not supporting user-defined value types.

C++ doesn't have the concept of "reference types", only "value types" (in C#-lingo); some of these value types can be pointers or references, but like value types in C#, none of them own their storage. If C++ treated "const" on its types the way C# treats "readonly" on value types, it would be very confusing as the example above demonstrates, nevermind the nasty interaction with copy constructors.

So C++ doesn't create a throw-away copy, because that would create endless pain. It doesn't forbid you to call any methods on members either, because, well, the language wouldn't be very useful then. But it still wants to have some notion of "readonly" or "const-ness".

C++ attempts to find a middle way by making you label which methods are safe to call on const members, and then it trusts you to have been faithful and accurate in your labeling and calls methods on the original objects directly. This is not perfect - it's verbose, and you're allowed to violate const-ness as much as you please - but it's arguably better than all the other options.

Const-correctness in C

I would generally use two of the three consts:

const char *const *my_strings;

Sometimes I would use all three, but in my opinion the last one is the least important. It only helps analyze the code that uses the variable my_strings, whereas the other two help analyze code that has any pointer to the array pointed to by my_strings, or to the strings pointed to by the elements of that array. That's generally more code, in several different places (for example the caller of a function and the function itself), and hence a harder task.

The code that uses the variable itself is limited to the scope of my_strings, so if that's an automatic variable (including a function parameter) then it is well-contained and an easier task. The help provided by marking it const might still be appreciated, but it's less important.

I would also say that if char const * const * const my_strings is "nearly unreadable", then that will change when you have more practice at reading C, and it's better to get that practice than to change the code. There's some value in writing C code that can be easily read by novices, but not as much value as there is in getting some work done ;-)

You could use typedefs to make the variable definition shorter, at the cost of introducing an indirection that will annoy many C programmers:

typedef char const *ro_strptr;
ro_strptr const *my_strings;

For whatever reasons, C programmers often want to see as much as possible of a type in one place. Only when the type gets genuinely complicated (pointer-to-function types) can you use a typedef solely to abbreviate, without half-expecting that somebody will complain about it.

Why do C standard libraries neglect const correctness?

Those are const-correct signatures.

You almost never write const before pass-by-value arguments.
The function gets its own copy so there's no danger there.

void *memcpy(void *dest, const void *src, size_t count)

is const correct too. Only the second pointer promises not to change what it points to. The destination pointer, on the other hand, is all about changing what it points to.

C++ Const correctness in C wrapper class

As far as I can tell all three functions in <pthread.h> take pthread_mutex_t* arguments. NOT pthread_mutex_t const* arguments.

So purely from the technical perspective you already have to make your three member functions non-const as a const member function will have access to this as Mutex const and its member variable mMutex as pthread_mutex_t const. Taking its pointer will then require a const_cast breaking const correctness.


But even if this wasn't the case, it wouldn't seem right to have a function with heavy side effects that can be called from a context that promised not to change the object.

So, in any case, your member functions aren't const.

const correctness in C#

I've come across this issue a lot of times too and ended up using interfaces.

I think it's important to drop the idea that C# is any form, or even an evolution of C++. They're two different languages that share almost the same syntax.

I usually express 'const correctness' in C# by defining a read-only view of a class:

public interface IReadOnlyCustomer
{
String Name { get; }
int Age { get; }
}

public class Customer : IReadOnlyCustomer
{
private string m_name;
private int m_age;

public string Name
{
get { return m_name; }
set { m_name = value; }
}

public int Age
{
get { return m_age; }
set { m_age = value; }
}
}

C++ Const Correctness in function parameters or in declartions

const at the end applies to this (making it pointer-to-const), meaning the member function will not modify the object on which it is called

class Cls {
public:
void f() {
++x_; // no problem
};

void g() const {
++x_; // error, can't modify in const member function
};

private:
int x_{};
};

In your example, you want to say both that the parameter is const, as well as this. In lhs == rhs, lhs is treated as const only if you have the trailing const, so you are right to use

bool operator==(const rational<T>& rat) const;
bool operator!=(const rational<T>& rat) const;

(though you should probably be omitting <T>)

Further, if you omit the trailing const, you would not be able to compare with a const object on the left

const rational<int> a;
const rational<int> b;
if (a == b) { // error if you don't have the trailing const on operator==

Is there const in C?

There are no syntactic differences between C and C++ with regard to const keyword, besides a rather obscure one: in C (since C99) you can declare function parameters as

void foo(int a[const]);

which is equivalent to

void foo(int *const a);

declaration. C++ does not support such syntax.

Semantic differences exist as well. As @Ben Voigt already noted, in C const declarations do not produce constant expressions, i.e. in C you can't use a const int object in a case label, as a bit-field width or as array size in a non-VLA array declaration (all this is possible in C++). Also, const objects have external linkage by default in C (internal linkage in C++).

There's at least one more semantic difference, which Ben did not mention. Const-correctness rules of C++ language support the following standard conversion

int **pp = 0;
const int *const *cpp = pp; // OK in C++

int ***ppp = 0;
int *const *const *cppp = ppp; // OK in C++

These initializations are illegal in C.

int **pp = 0;
const int *const *cpp = pp; /* ERROR in C */

int ***ppp = 0;
int *const *const *cppp = ppp; /* ERROR in C */

Generally, when dealing with multi-level pointers, C++ says that you can add const-qualification at any depth of indirection, as long as you also add const-qualification all the way to the top level.

In C you can only add const-qualification to the type pointed by the top-level pointer, but no deeper.

int **pp = 0;
int *const *cpp = pp; /* OK in C */

int ***ppp = 0;
int **const *cppp = ppp; /* OK in C */

Another manifestation of the same underlying general principle is the way const-correctness rules work with arrays in C and C++. In C++ you can do

int a[10];
const int (*p)[10] = &a; // OK in C++

Trying to do the same in C will result in an error

int a[10];
const int (*p)[10] = &a; /* ERROR in C */

Sell me on const correctness

This is the definitive article on "const correctness": https://isocpp.org/wiki/faq/const-correctness.

In a nutshell, using const is good practice because...

  1. It protects you from accidentally changing variables that aren't intended be changed,
  2. It protects you from making accidental variable assignments, and
  3. The compiler can optimize it. For instance, you are protected from

    if( x = y ) // whoops, meant if( x == y )

At the same time, the compiler can generate more efficient code because it knows exactly what the state of the variable/function will be at all times. If you are writing tight C++ code, this is good.

You are correct in that it can be difficult to use const-correctness consistently, but the end code is more concise and safer to program with. When you do a lot of C++ development, the benefits of this quickly manifest.



Related Topics



Leave a reply



Submit