C++ const object's member can be modified - c++

In this code the object bar is an const type, but through the const function I still can modify the member x's value. Is that unreasonable?
the output is
15
25
// overloading members on constness
#include <iostream>
using namespace std;
class MyClass {
int x;
public:
MyClass(int val) : x(val) {}
int& get() const {return x;}
int& get() {return x;}
};
int main() {
MyClass foo (10);
const MyClass bar (20);
foo.get() = 15;
bar.get() = 25;
cout << foo.get() << '\n';
cout << bar.get() << '\n';
return 0;
}

int& get() const {return x;}
is returning a non-const reference to a member of a const object. (We know that *this is const because of the declaration of int& get() as const.) That should be flagged as an error, since it is an invalid conversion (x is not declared mutable); both gcc and clang will do so. The fact that your compiler only produced a warning is odd, but nonetheless you should heed the warning.
You could avoid the error by explicitly using const_cast<int&>(x), but it would be undefined behaviour (UB) to attempt to use the returned int& to modify x. However, the compiler is not obliged to flag as error or even detect all possible expressions which might produce undefined behaviour.
In short, you're allowed to shoot yourself in the foot, but a good compiler will at least warn you before you do that. Listen to the warnings.

Related

function redefinition: const parameter

1. In global scope, this gives error: redefinition of 'f'
#include <iostream>
using namespace std;
void f(int x) { cout << "f" << endl; }
void f(const int x) { cout << "f (const)" << endl; } // error: redefinition of 'f'
int main() { }
2. Defining two copy constructors (one with const, the other without) compiles
#include <iostream>
using namespace std;
class Foo {
public:
Foo(const Foo&) { cout << "copy (const)" << endl; }
Foo(Foo&) { cout << "copy" << endl; }
};
int main() { }
Question
Why is #1 a redefinition error but #2 is not?
For the second example, is there a use case for defining two copy constructors (one with const the other without)?
Only the top-level constness is ignored on parameters when checking if two functions are the same.
What does "top-level" constness mean? It means that something is actually const, as reported by std::is_const_v.
For example int *const is top-level const (because the pointer itself is const), and const int * is not (because the pointer itself is not const, even though it points to something that is const).
Something can be const at several levels, e.g. const int *const.
const int is also const at the top level, because there's only one "level" here.
If you have more than one star (e.g. int ***), then the type is top-level const only if const is placed after the rightmost star.
So, const int is const at the top level, meaning const int and int only differ in top-level constness.
But (similarly to const int *) const Foo& is const not at the top-level. It's a non-const reference to const Foo. (The references can never be const1, e.g. Foo &const doesn't compile.)
So the difference between Foo & and const Foo & is not on the top level, making Foo(Foo &) and Foo(const Foo &) different constructors.
1 Some argue that all references are effectively const because you can't make them point to a different object after they're created. But the language says they're not const, and std::is_const_v returns false for them.
There is a fundamental difference between the two.
One is an overload between int and const int. It's a value type. There is no semantic difference for the caller, the effect of const only affects the body of the function.
void f(int);
int a = 1;
const int b = 2;
f(a); // must copy the int value into the argument
f(b); // same thing.
The other is a const vs a mutable reference. It has a difference for the caller.
void f(int&);
void f(const int&);
int a = 1;
const int b = 2;
f(a); // could call f(int&) or f(int const&), but the mutable is a more closely match
f(b); // can only call f(int const&);
Since its passed by reference, the constness matter for the caller of the function. A function that tries to mutate a const object by reference must be invalid, and a non const object should be passed to the non const overload by default.
With only values, it don't matter at all. It is a new object. No matter the qualifier, it has no meaning for the caller, and it should therefore not care since it only affect the implementation.
You can even add const in definitions only if you want, since it declares the same function:
void f(int);
int main() {
f(1);
}
void f(const int a) {
std::cout << "hello " << a << std::endl;
}
Live example
As for your second question, I would say that since the addition of rvalue reference, there is little need for a copy constructor to take by mutable reference.
For example, std::auto_ptr used to have a constructor that took a mutable reference to transfer ownership, but it created all sorts of problems. But it has been completely replaced by std::unique_ptr, which uses rvalue reference to transfer ownership.
Rvalue reference ensure that you don't care for the integrity of the copied-from object, and that it's okay to steal off resources from it.
#1 is a redefinition error because even if you modify the local x it is passed by value so after the return call the value will stay the same.

Understanding const

If you want to write an iterator, you usually write
T* operator->() const;
My problem is understanding this "const" with pointers and reference.
For instance, you can write the following struct:
struct A{
int* x;
A(int& x0):x{&x0}{}
int* ptr() const {return x;}
int& ref() const {return *x;}
};
And you can use it this way:
int x = 10;
const A a{x};
int& r = a.ref();
int* p = a.ptr();
*p = 4;
cout << x << endl; // prints 4
r = 25;
cout << x << endl; // prints 25
But why this compiles and works right (at least with g++ and clang). Why?
As I defined
const A a{x};
this "a" is const. So when I call
int* p = a.ptr();
I am calling ptr() with a const object, so the internal pointer A->x must be "int * const". But I am returning a "int *" without const. Why is this correct?
And what happens with the reference? If I call A::ref() with a "const A", what's the type this function returns? Something like "int& const"??? <--- I suppose this is the same as "int&".
Thanks for your help.
There is a different between bitwise const and logical const.
When you have a const A, you can't modify its int* member. But there's a difference between modifying the member itself (which int that x points to) and modifying through the member (the value of the int that x points to). Those are different kinds of const. In the simplest case:
struct Ptr { int* p; };
int i = 42;
const Ptr ptr{&i};
*ptr.p = 57;
ptr.p still points to i, nothing changed there, so the const mechanic is enforced. But it's not logically const since you still changed something through a const object. The language doesn't enforce that for you though. That's up to you as the library writer.
If you want to propagate const-ness, you just provide an interface that is logically const:
int const* ptr() const {return x;}
int const& ref() const {return *x;}
// ^^^^^
Now, users can't modify through your const A both bitwise (can't change what x points to) and logically (can't change that value of that pointee either).
But why this compiles and works right (at least with g++ and clang). Why?
Because the program is well formed and has defined behaviour. Const correctness was not violated.
I am calling ptr() with a const object, so the internal pointer A->x must be "int * const". But I am returning a "int *" without const. Why is this correct?
Because it is completely OK to make copies of const objects. Those copies need not to be const. Copying an object does not make modifications to the original object (assuming there is no user defined copy constructor that does silly things).
And what happens with the reference? If I call A::ref() with a "const A", what's the type this function returns?
int& ref() always returns int&. Just like int* ptr() always returns int*.
Something like "int& const"???
There is no such thing like int& const. References cannot have top level qualifiers (they can never be re-assigned).
In struct A, when you make an instance of it const, you make the pointer constant, but that doesn't automatically make the pointed-to object constant.
Declaring something like const A a(ref); is basically equivalent to invoking the following code:
struct A_const {
int * const x;
A(int& x0):x{&x0}{}
int* ptr() const {return x;}
int& ref() const {return *x;}
};
If you remember your pointer rules, this means that x is a constant pointer, which means it cannot be made to point to something else (it's functionally similar to a reference, but can be null), but critically, the int that it is pointing to is not constant, which means nothing stops you from writing something like this:
int val = 17;
const A a(val);
*(a.val) = 19; //Totally valid, compiles, and behaves as expected!
int val2 = 13;
//a.val = &val2; //Invalid, will invoke compile-time error
This is also the reason why std::unique_ptr<int> and std::unique_ptr<const int> represent different objects.
If you want the pointed-to object to not be modifiable on a const object, you need to enforce that in the code itself. Since functions can be overloaded on the basis of whether the source object is const or not, that's pretty easy:
struct A {
int * x;
A(int& x0):x{&x0}{}
int * ptr() {return x;}
int & ref() {return *x;}
int const* ptr() const {return x;}
int const& ref() const {return *x;}
};
int val = 17;
A a(val);
a.ref() = 19;//Okay.
*a.ptr() = 4;//Okay.
const A b(val);
b.ref() = 13;//Compile error
*b.ptr() = 17;//Compile error

Writing to class member through const &

In this example, is the c-style cast to int& followed by an assignment to kind of hack the interface of class A undefined behavior?
class A
{
public:
A()
: x(0)
{
}
~A()
{
std::cout << x << std::endl;
}
const int& getX()
{
return x;
}
private:
int x;
};
int main()
{
A a;
int& x = (int&)a.getX();
x = 17;
std::cout << x << std::endl;
}
Output:
17
17
If so, what part of the standard can i refer to? Also, is there any reason why this compiles without warnings? (i tested with c++14 on cpp.sh with -Wall, -Wextra and -Wpedantic)
const int& getX() { return x; }
Since this method is not marked const, x is a mutable int. A reference is taken and cast to a const int& at the point of return. Note that although the reference is to a const int, the actual referee int is mutable. This is important.
int& x = (int&)a.getX();
This line takes the returned const int reference and const_cast's it to an int reference. This is legal in c++, full stop. [expr.const.cast]
However, writing through this reference is only legal if the original object being referenced is mutable.
In this case, it is.
You will find the details in [dcl.type.cv]

Overloading Based on L-Value versus R-Value

I found in a C++ book the following:
Although we will not be doing it in this book, you can overload a
function name (or operator) so that it behaves differently when used
as an l-value and when it is used as an r-value. (Recall that an
l-value means it can be used on the left-hand side of an assignment
statement.) For example, if you want a function f to behave
differently depending on whether it is used as an l-value or an
r-value, you can do so as follows:
class SomeClass {
public:
int& f(); // will be used in any l-value invocation const
const int& f( ) const; // used in any r-value invocation ...
};
I tried this and it didn't work:
class Foo {
public:
int& id(int& a);
const int& id(int& a) const;
};
int main() {
int a;
Foo f;
f.id(a) = 2;
a = f.id(a);
cout << f.id(a) << endl;
}
int& Foo :: id(int& a) {
cout << "Bar\n";
return a;
}
const int& Foo :: id(int& a) const {
cout << "No bar !\n";
return a;
}
Have I wrongly understood it ?
Either the book's example is flat-out wrong, or you copied the wrong example from the book.
class SomeClass {
public:
int& f(); // will be used in any l-value invocation const
const int& f( ) const; // used in any r-value invocation ...
};
With this code, when you call s.f() where s is an object of type SomeClass, the first version will be called when s is non-const, and the second version will be called when s is const. Value category has nothing to do with it.
Ref-qualification looks like this:
#include <iostream>
class SomeClass {
public:
int f() & { std::cout << "lvalue\n"; }
int f() && { std::cout << "rvalue\n"; }
};
int main() {
SomeClass s; s.f(); // prints "lvalue"
SomeClass{}.f(); // prints "rvalue"
}
Ofcourse the book is correct. Let me explain the workings of an example of what the author meant :
#include <iostream>
using namespace std;
class CO
{
int _m;
public:
CO(int m) : _m(m) {}
int& m() { return _m; } // used as an l-value
int const& m() const { return _m; } // used as an r-value
};
int main()
{
CO a(1);
cout << a.m() << endl;
a.m() = 2; // here used as an l-value / overload resolution selects the correct one
cout << a.m() << endl;
return 0;
}
Output is
1
2
What you misunderstood is the function signature. You see when you have an argument &arg (as in id(&arg)) you pretty much predefine the l-valuness of it, so returning it through a const or non const member function does not change a thing.
The author refers to a common writting style that allows for 'getters' and 'setters' to be declared with a signature different only in const qualifires yet compile and behave correctly.
Edit
To be more pedantic, the following phrase
Recall that an l-value means it can be used on the left-hand side of an assignment statement.
is not valid anymore. lr valuness applies to expressions, and the shortest way to explain it, is that an expression whose adress we can take, is an l-value; if it's not obtainable it's an r-value.
So the syntax to which the author refers to, enforces the member function to be used correctly (correct compilation / overload resolution) at both sides of the assignment operator. This nowdays is no longer relevant to lr valueness.
A const member function can only be called on a const object. It makes no difference what you do with the return value. In your example, f is non-const, so it always calls the non-const version of f(). Note that you can also overload on r-value references (&&) in C++11.

Is the compiler allowed to select const ref over ref during overload resolution?

Some background:
I came across something the other day that got me thinking about overload resolution in nested function calls. Consider the code below:
#include <iostream>
void printer(const int &a)
{
std::cout << a << "\n";
}
const int& func(const int &a)
{
std::cout << "const int& ";
return a;
}
int& func(int &a)
{
std::cout << "int& ";
return a;
}
int main()
{
int a = 42;
const int b = 21;
printer(func(a));
printer(func(b));
return 0;
}
This code prints
int& 42
const int& 21
So, obviously func(a) sees that a is a non-const int. The compiler must also see that the printer function wants a const int& argument. And there exists a func(...) that returns a const int&. I looked into the C++ standard and it says that const refs and refs are considered distinct parameter types (and that is why it picks the int& for func(a)).
To the question:
Is the compiler allowed to use the func(const int &) version instead of the func(int&) when calling func(a)?
(Perhaps there is some kind of optimization possibility if it sees that the result is passed to a function wanting a const int& parameter.)
Overload resolution does not consider the return type. That is, it will only look at the argument to the function and the different overloads, disregarding how the returned value is going to be used.
For a more explicit test, consider changing the non-const overload to:
void func( int& ) {}
which will fail to compile, even if there is a different similar overload that would allow the code to compile.