Does this C++ static analysis rule make sense as is? - c++

I'm implementing some C++ static analysis rules, and one of them prohibits a function from returning a reference or pointer to a reference parameter of the function, i.e. the following are all non-compliant:
int *f(int& x) { return &x; } // #1
const int *g(const int& x) { return &x; } // #2
int& h(int& x) { return x; } // #3
const int& m(const int& x) { return x; } // #4
The justification given for this is that "It is implementation-defined behaviour whether the reference parameter is a temporary object or a reference to the parameter."
I'm puzzled by this, however, because stream operators in C++ are written in this way, e.g.
std::ostream& operator<<(std::ostream& os, const X& x) {
//...
return os;
}
I think I'm pretty confident that stream operators in C++ do not in general exhibit implementation-defined behaviour, so what's going on?
According to my understanding as it is at present, I would expect #1 and #3 to be well-defined, on the basis that temporaries cannot be bound to non-const references, so int& x refers to a real object that has lifetime beyond the scope of the function, hence returning a pointer or reference to that object is fine. I would expect #2 to be dodgy, because a temporary could have been bound to const int& x, in which case trying to take its address would seem a bad plan. I'm not sure about #4 - my gut feeling is that that's also potentially dodgy, but I'm not sure. In particular, I'm not clear on what would happen in the following case:
const int& m(const int& x) { return x; }
//...
const int& r = m(23);

As you say, #1 and #3 are fine (though #1 is arguably bad style).
#4 is dodgy for the same reason #2 is; it allows propagating a const reference to a temporary past its lifetime.
Let's check:
#include <iostream>
struct C {
C() { std::cout << "C()\n"; }
~C() { std::cout << "~C()\n"; }
C(const C &) { std::cout << "C(const C &)\n"; }
};
const C &foo(const C &c) { return c; }
int main() {
const C &c = foo(C());
std::cout << "c in scope\n";
}
This outputs:
C()
~C()
c in scope

In C++11, #2 and #4 can be made safe if there are also rvalue reference overloads. Thus:
const int *get( const int &x ) { return &x; }
const int *get( const int &&x ) { return nullptr; }
void test() {
const int x = 0;
const int *p1 = get( x ); // OK; p1 is &x.
const int *p2 = get( x+42 ); // OK; p2 is nullptr.
}
So although they are dodgy, they do have safe uses if the programmer knows what they are doing. It'd be draconian to forbid this.
(Perhaps safer would be if the const rvalue reference overload was made private, left undefined, or otherwise caused a compile-time or link-time error. This is especially true for the #4 case, where we return a reference but there is nothing good to return a reference to and the language doesn't allow references to null.)

Related

const_cast: modifying a formerly const value is only undefined if the original variable is const

https://stackoverflow.com/a/332086/462608
modifying a formerly const value is only undefined if the original variable is const
...
if you use it to take the const off a reference to something that wasn't declared with const, it is safe.
...
This can be useful when overloading member functions based on const, for instance. It can also be used to add const to an object, such as to call a member function overload.
I am unable to understand the meanings of the above quotes. I request you to give me examples to practically show what these quotes mean.
Regarding your first two quotes:
void do_not_do_this(const int& cref) {
const_cast<int&>(cref) = 42;
}
int main() {
int a = 0;
// "if you use it to take the const off a reference
// to something that wasn't declared with const, it is safe."
do_not_do_this(a); // well-defined
// a is now 42.
// "modifying a formerly const value is only
// undefined if the original variable is const"
const int b = 0;
do_not_do_this(a); // undefined behavoiur
}
Regarding your final quote:
// "This can be useful when overloading member functions based
// on const, for instance. It can also be used to add const
// to an object, such as to call a member function overload."
class A {
const int& get() const
{
// ... some common logic for const and
// non-const overloads.
return a_;
}
int& get() {
// Since this get() overload is non-const, the object itself
// is non-const in this scope. Moreover, the a_ member
// is non-const, and thus casting away the const of the return
// from the const get() (after 'this' has been casted to
// const) is safe.
A const * const c_this = this;
return const_cast<int&>(c_this->get());
}
private:
int a_{0};
}
How about this:
#include <iostream>
void foo(const int& ub, const int& ok)
{
const_cast<int&>(ub) = 0.0; // undefined behaviour - the original object is const
const_cast<int&>(ok) = 1.0; // this is fine - the original object is not const
}
int main()
{
const int ub = 1.0;
int ok = 0.0;
foo(ub, ok);
std::cout << ub << " " << ok << std::ends;
}
Note the output on common compilers is 1 1: the rationale being that the compiler knows that ub cannot change in main so it substitutes 1 for ub in the std::cout call.
Your third paragraph is alluding to a function body of a non-const member function calling the const version as a means of obviating code repetition.

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]

Access to reference in member variable discards constness

I made a wrapper around an object in my code that should modify accesses to the object. I choose to use an object here for testing instead of a functor that would have the same functionality. Basically: The wrapper receives a reference to the object and forwards all indexed accesses to the object (after some possible manipulation)
Now comes the problem: The accessor discards constness of the wrapped object.
Minimal Example
struct Foo
{
std::array<int, 2> data;
const int& operator()(int idx) const{
return data[idx];
}
int& operator()(int idx){
return data[idx];
}
};
struct Bar
{
Foo& ref;
Bar(Foo& r):ref(r){}
int& operator()(int idx) const{
return ref(idx);
}
};
template< typename T >
void test(const T& data){
data(1) = 4;
std::cout << data(1);
}
void main(){
Foo f;
test(f);
// Above call does not compile (as expected)
// (assignment of read-only location)
Bar b(f);
test(b); // This does compile and works (data is modified)
}
Declaring the ()-operator of Bar (the wrapper) "const", I'd expect to be all member accesses "const" to. So it shouldn't be possible to return an "int&" but only a "const int&"
However gcc4.7 happily compiles the code and the const is ignored. Is this the correct behavior? Where is this specified?
Edit:
On a related issue: If use typedefs in Foo like:
struct Foo
{
using Ref = int&;
using ConstRef = const int&; //1
using ConstRef = const Ref; //2
int* data; // Use int* to have same issue as with refs
ConstRef operator()(int idx) const{
return data[idx]; // This is possible due to the same "bug" as with the ref in Bar
}
Ref operator()(int idx){
return data[idx];
}
};
I noticed that //1 does work as expected but //2 does not. Return value is still modifiable. Shouldn't they be the same?
Yes, this is correct behaviour. The type of ref is Foo &. Adding const to a reference type1 does nothing—a reference is already immutable, anyway. It's like having a member int *p. In a const member function, its type is treated as int * const p, not as int const * p.
What you need to do is add const manually inside the const overload if you want it there:
struct Bar
{
Foo& ref;
Bar(Foo& r):ref(r){}
int& operator()(int idx) const{
return const_cast<const Foo&>(ref)(idx);
}
};
To address the question edit: no, the typedefs are not the same. const int & is a reference to a (constant int). const Ref is a constant Ref, that is, a constant (reference to int); parentheses used in mathematical sense.
1 I am talking about the reference type itself. Not to be confused with adding const to the type to which the reference refers.
Yeah, it is expected behaviour. The reason is that const for your method says only that reference wont be change not the referenced object. Reference is always unchanged so it is always true. Take a look at this code with pointer:
int i;
struct Bar
{
int* pi;
Foo& ref;
Bar(Foo& r):ref(r){}
int& operator()(int idx) const{
*pi = 4; // we can change pointed object
pi = &i; // Compile error: we can't change the pointer.
return ref(idx);
}
};

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.

C++ const object's member can be modified

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.