Const Correctness for Value Parameters

Const correctness for value parameters

My take on it:

It's not a bad idea, but the issue is minor and your energy might be better spent on other things.

In your question you provided a good example of when it might catch an error, but occasionally you also end up doing something like this:

void foo(const int count /* … */)
{
int temp = count; // can't modify count, so we need a copy of it
++temp;

/* … */
}

The pros and cons are minor either way.

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==

Const correctness of function parameters, that are passed by value

Your point is well taken. And depending on the language, the compiler/interpreter may throw an error or warning when it sees code like your first example.

However, at some point you have to choose whether or not you are going to try and protect "the developers" from doing something stupid or just assume that they are and catch this sort of thing during a code review.

Utilizing syntactic mechanisms to make the code safer is a good thing, IMHO. It can sort of impede development flow, unfortunately.

Why using const for arguments passed by value?

I can think of a few reasons:

1) When someone reads the code and see const T a, they know that a should not be modified in the body of the function.

2) The compiler will tell you when you try to modify a in the body of the function. Therefore, adding const can prevent mistakes.

BTW chris already mentioned this in the comments.

3) However, there is another difference in C++11. A constant object cannot be moved from, as a move operation modifies the object. Therefore, you can only make a copy of a in the function body and cannot move from it.

4) Also, if this is a class type, you cannot call non-const members functions on a const object.

C++: Const correctness and pointer arguments

You have it backwards:

const int * intPtr1; // Declares a pointer whose contents cannot be changed.
int * const intPtr2; // Declares a pointer that cannot be changed.

The following const is indeed unnecessary, and there's no reason to put it in a function declaration:

void someFunc1(int * const arg);

However, you might want to put it in the function implementation, for the same reason that you might want to declare a local variable (or anything else) const - the implementation may be easier to follow when you know that certain things won't change. You can do that whether or not it's declared const in any other declarations of the function.

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.

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.

Pass-by-value/const correctness with C++11

Regarding the first part, this seems basically fine, but read to the end of this answer. For the second part, you should first know that top-level const is stripped from the function's signature. So this

void foo( Color c );

is identical to

void foo( const Color c );

For the caller, it therefore makes no difference.
The only difference is when you define the function, if you put const to a parameter for the definition, the compiler won't modify it. This doesn't make sense in the context you are asking the question: If you want to move the instance somewhere else, it is not const.

If you think const is the right way to go, use a const reference. Now back to your first question: Using void f( Color c ); seems easy and in some cases, superfluous copies are avoided. The problem is that this does not work in all cases and on todays compilers. Compared to overloading const Color& and Color&&, sometimes an additional move is generated. The only benefit is that you need fewer overloads, which might become important once you have multiple parameters.

To explain the difference with code:

void X::f( Color c )
{
this->c = std::move(c); // 1 move=
}

void X::g( const Color& c )
{
this->c = c; // 1 copy=
}

void X::g( Color&& c )
{
this->c = std::move(c); // 1 move=
}

X x;
Color c;
x.f(c); // 1 copy-ctor, 1 move=
x.g(c); // 1 copy=
x.f(Color()); // 1 ctor, 1 move=
x.g(Color()); // 1 ctor, 1 move=
x.f(std::move(c)); // 1 move-ctor, 1 move=
x.g(std::move(c)); // 1 move=

As you can see, the "traditional" way of using a const reference combined with a C++11 rvalue-reference overload has advantages wrt the number of move-operations (which still do have a cost). Balance it with the simplification of not having overloads and judge for yourself.

Why const correctness rule doesn't work for built in libraries?

To start, unlearn what you think you know. Let's look at what it means to be a const member.

class D {
public:
int fun2();
int fun3() const;
};

What does this declare? There is a class called D. There are two member functions fun2 and fun3, each taking a hidden this parameter and no other parameters.

Hold on! A hidden parameter? Well, yes. You can use this within the function; its value has to come from somewhere. All non-static member functions have this hidden parameter. However, not all non-static member functions have the same type of hidden parameter. If I were to show the hidden parameter, the declarations would look like the following:

int D::fun2(D * this);
int D::fun3(const D * this);

Notice how the const exists inside this pseudo-declaration? That is the effect of declaring a const member function: this points to a const object rather than a non-const object.

Now back to the question. Can fun3 call fun2? Well, fun3 would pass its this pointer (a pointer-to-const-object) to fun2, which expects a pointer-to-object. That would mean losing constness, so it's not allowed.

Can fun3 call abs? Well, fun3 would pass an integer to abs. No problem here. The problem is losing the constness of this. As long as you avoid that, you're fine.



Related Topics



Leave a reply



Submit