Dan
Dan

Reputation: 2886

Using reinterpret_cast to pass a value by reference

#include <iostream>

void inc(long& in){
 in++;
}

int main(){
 int a = 5;
 inc(*reinterpret_cast<long*>(&a));

 printf("%d\n", a);
 return 0;
}

Above code compiles successfully and prints 6. Is it undefined behaviour? as I'm not really making a reference to anything on the stack. I'm doing an inline cast.

Note that this will not work with a normal cast such as static_cast<long>(a), you will get a compiler error saying:

candidate function not viable: expects an l-value for 1st argument

Why does *reinterpret_cast<long*>(&a) compile? I'm dereferencing it to a long so the compiler should be able to tell it's not referencing anything.

Edit

Ignore the int/long difference here please, that's not the point of my question. Assume this example if it makes more sense:

void inc(int& in){...}
...
inc(*reinterpret_cast<int*>(&a));

My question is, how is it ok to pass *reinterpret_cast<int*>(&a) as a reference but static_cast<int>(a) isn't?

Upvotes: 0

Views: 915

Answers (3)

David Dolson
David Dolson

Reputation: 66

Let's break it down. Create some memory for a variable

 int a = 5;

If int is 32 bits on a little endian machine, it looks like this in memory:

0x05 0x00 0x00 0x00

Then you take the address of it, which creates an int pointer, and cast it to a long pointer. Reinterpret_cast assumes you know that the memory actually contains a long.

Let's assume long is 64 bits on your machine. The pointer now points to the 8 bytes of a plus some other stuff on the stack

05 00 00 00  aa bb cc dd

Then you pass the dereferenced long pointer by reference to the inc() function. Pass by reference actually passes the pointer. So inc() gets the address of the 05 00 00 00 aa bb cc dd memory and treats it as a long, increments it, and stores 06 00 00 00 aa bb cc dd in memory. When your code looks at the value of 'a', the 4 bytes 06 00 00 00 give it the value 6.

As you should be thinking, it is a lucky coincidence that it creates the correct answer.

As has been said, this is legal code because reinterpret_cast is a directive to override type safety.

I consider reinterpret_cast to be a bad code smell. Don't use it unless you are writing bytes to binary data or a device driver. Certainly never use it because you can't get static_cast to compile.

And as for static_cast(a), this expression is just a value. In simple terms, because it is not a variable at an address, it has no address, and hence cannot be passed by reference.

Upvotes: 1

Daniel Langr
Daniel Langr

Reputation: 23497

As for the editted question:

My question is, how is it ok to pass *reinterpret_cast<int*>(&a) as a reference but static_cast<int>(a) isn't?"

As it was already said, this is because the *reinterpret_cast<int*>(&a) expression has a value category lvalue as described in [expr.unary.op]:

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.

On the contrary, the static_cast<int>(a) expression has a category prvalue (which is rvalue at the same time) according to [expr.static.cast]:

The result of the expression static_­cast<T>(v) is the result of converting the expression v to type T. If T is an lvalue reference type or an rvalue reference to function type, the result is an lvalue; if T is an rvalue reference to object type, the result is an xvalue; otherwise, the result is a prvalue.

And, rvalues cannot be bound to non-const lvalue references (type of the function parameter).

Note that both expressions refer to the very same object, but "through" different categories. (Another case of how to change the category of an object to rvalue would be std::move(a)).

Upvotes: 1

eerorika
eerorika

Reputation: 238361

Is it undefined behaviour?

Yes.

Why does *reinterpret_cast<long*>(&a) compile?

Because it is well-formed. By using reinterpret_cast, you are telling the compiler that you know what you're doing, and that it's going to be correct. The compiler has no choice but to believe you. The issue is that it wasn't correct and you didn't know.

It would be fine if the address actually contained an object of type long. For example, the following would have well defined behaviour (because standard layout class and its first member are pointer-interconverible):

struct T {
    long l;
} a {42};

// no problem
inc(*reinterpret_cast<long*>(&a));

Another case where this sort of reinterepretation is allowed is narrow character types and std::byte. It's probably never used with a reference in practice, but this is technically OK:

void inc(std::byte&);
int a = 42;
// you will get the first byte
inc(*reinterpret_cast<std::byte*>(&a));

Upvotes: 2

Related Questions