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?
- 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.
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.
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
Access Array Beyond the Limit in C and C++
How to Sort a Std::Map First by Value, Then by Key
Visual Studio 2015 Doesn't Have Cl.Exe
The Copy Constructor and Assignment Operator
Why Is Initializing an Integer in C++ to 010 Different from Initializing It to 10
How to Reset Std::Cin When Using It
Compare Two Float Variables in C++
*.H or *.Hpp for Your Class Definitions
Std::Unique_Lock<Std::Mutex> or Std::Lock_Guard<Std::Mutex>
C++ Preprocessor: Avoid Code Repetition of Member Variable List
In What Ways Do C++ Exceptions Slow Down Code When There Are No Exceptions Thown
How to Use C++11 Uniform Initialization Syntax
Where Are Temporary Object Stored
Why Floating Point Value Such as 3.14 Are Considered as Double by Default in Msvc