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 achar 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?
- fail to compile
- print "3, 4"
- 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 const
s:
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...
- It protects you from accidentally changing variables that aren't intended be changed,
- It protects you from making accidental variable assignments, and
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
Why Is Operator!= Removed in C++20 for Many Standard Library Types
How to Increment an Iterator by Just Adding a Number
Brace Elision in Std::Array Initialization
Cannot Create Constexpr Std::Vector
C++, How to Statically Initialize a Std::Map at Compile Time
What Is the Ndebug Preprocessor MACro Used for (On Different Platforms)
Pure/Const Function Attributes in Different Compilers
Pointer to Array of Unspecified Size "(*P)[]" Illegal in C++ But Legal in C
Why Does Valgrind Say Basic Sdl Program Is Leaking Memory
Incrementing Iterators: Is ++It More Efficient Than It++
Interesting Behavior of Compiler with Namespaces
Handling Overflow When Casting Doubles to Integers in C
Why Doesn't C++ Make the Structure Tighter
Why Can't Templates Be Declared in a Function
How to Avoid Entering Library's Source Files While Debugging in Qt Creator with Gdb