Wednesday, January 25, 2012

C++ reference internals

Hmm ... where to start. 

Ok .. a reference is a C++ enhancement on top of the traditional C pointers. So, a reference is NOT a totally new invention of sort. This enhancement was added to pointers, to address one main issue that exist with pointers :

Pointers are too powerful and hence prone to mishandling.
So .. are pointers evil ? Nope ... Address manipulation is basically a low level operation and pointers are masters in doing that. Pointers are susceptible to errors, since it gives too much freedom to the programmer. Consider the following program:

void function() {
  uint32_t fourGig = 0xFFFFFFFF;
  uint32_t *ptr = ( uint32_t * ) 0x1;

  write( 1, ptr, fourGig );
}
 
The above code "attempts" to dump the entire address space of the current running process on a 32-bit host to stdout. On most systems, this would probably fail. But this is just shown to demonstrate the possibilities that a pointers carries. A program normally contains data and code. Code is mostly non-modifiable. And guess what ?  Pointers can totally manipulate the data part , making them kinda a "queen" in a game of Chess. Pointers are powerful, and with great power comes great responsibility, and so is the programmer expected to be. But, everyone is prone to committing mistakes, and trying to reduce the chances of recommitting the mistakes is an intelligent decision. "References" are a choice in that direction.

References are "pointers" in one sense and are "objects" in another sense. I know that most definitions to references are quirky, and I don't want to add to it. By the time, we're done with this post, we should have a much clear definition.

Ok .. let's get started. Consider the following program:

int main() {
  int a = 0x11111111;
  int &b = a;

  return 0;
}

The assembly code of the main function is displayed below.

(gdb) disassemble main
Dump of assembler code for function main:
0x00001f70 <main+00>:    push   %ebp
0x00001f71 <main+01>:    mov    %esp,%ebp
0x00001f73 <main+03>:    sub    $0x10,%esp
0x00001f76 <main+06>:    movl   $0x11111111,-0xc(%ebp)
0x00001f7d <main+13>:    lea    -0xc(%ebp),%eax
0x00001f80 <main+16>:    mov    %eax,-0x10(%ebp)

0x00001f83 <main+19>:    movl   $0x0,-0x8(%ebp)
0x00001f8a <main+26>:    mov    -0x8(%ebp),%eax
0x00001f8d <main+29>:    mov    %eax,-0x4(%ebp)
0x00001f90 <main+32>:    mov    -0x4(%ebp),%eax
0x00001f93 <main+35>:    add    $0x10,%esp
0x00001f96 <main+38>:    pop    %ebp
0x00001f97 <main+39>:    ret   
End of assembler dump.

Consider the lines in bold.  

-0xc(%ebp) is the address of local variable "a",   
-0x10(%ebp) is the address of local reference variable "b"

The first line assigns 0x11111111 to variable "a". The second line is significant. The "lea" instruction "loads effective address" to register eax. In simple terms, "lea" moves the value -0xc(%ebp) ( which is &a ) to eax register. The next line moves the address ( &a ) to -0x10(%ebp) ( location of variable b ). So, the above code actually mean:

int main() {
  int a = 0x11111111;
  int &b = &a;

  return 0;
}

The point here is that references actually stores the address of the object / variable assigned to it, which makes them pointers. Don't believe it ? Let's modify the above program to use pointers, as below: 

int main() {
  int a = 0x11111111;
  int *b = &a;

  return 0;
}

Disassembling the  above code should give the "exact" same set of instructions, as the code for reference. Check it for yourself !! If they have the same instructions, then what's the point in using references ? Well .. that's the interesting part. Let's start with the below line.

int &b = a;

When the above line is encountered by the compiler, it converts the code internally to the below one:

int &b = &a;

How does the compiler get "address of a" from "value a" ? Good point. The compiler knows where "a" is allocated. So, it exploits this knowledge, and gets the address, whenever you specify a variable such as "a". The compiler doesn't need "&a" for this. What difference does this all make ? A world of difference. First, you are not allowed to handle "&a", an address.

You are only allowed to play around variable "a", not its address. The compiler handles the address part for you. When you don't play around with addresses, you don't need pointers. Less causality. Simple !

Hmm .. ok ... so that takes care of assigning values to the reference ... What about using them ? The answer is simple. Consider the following program:

 int main() {
  int a = 0x11111111;
  int &b = a;
  int c = b;

  return 0;
}

We already know that references deal with "variable addresses" internally and not values. So, when the compiler encounters 

int c = b;

it just converts it to

int c = *b;
Hmm ... so references are indeed pointers internally !!! This gives rise to an interesting question. Is the below line valid ?

int main() {
  int &b = 0x11111111;

  return 0;
}

NOPE .... The compiler will give a message similar to the one below:

 error: invalid initialization of non-const reference of type 'int&' from a temporary of type 'int'

 The problem as to why this failed should be obvious by now. We don't have an address to operate on. References need a variable ( from which it can extract an address ) to work. In short, references need an lvalue as an argument. Okay good ... but why does the below code work then ? It should fail too right ?

 int main() {
  const int &b = 0x11111111;

  return 0;
}

Actually, this is not valid code for the same reason mentioned above. But there is a catch here. We're operating on a "const variable". And const variables don't change value. The compiler exploits this knowledge. In this specific case, the compiler automatically creates a local variable, copies the rvalue to it, and uses it's address as shown below:

 int main() {
  int temp = 0x11111111;
  const int &b = temp;

  return 0;
}

This local variable "temp" is called a "temporary" in C++ jargon. This temporary variable has the same scope of the reference. That is, it will stay as long the reference stays. If you've noticed, there is something else that's interesting here. This code is exactly the same code that we wrote initially ( using variable name 'a' rather than 'temp' ). There is no variable naming in assembly. So, the assembly code for both the above and below code should exactly match !!!! Check it out for yourself.

So, should we always use references ?


Well .. the point is to use it as much as possible. It's not always possible to use references. References don't have all the power of pointers. Main reason being they can't be empty or re-assigned to point to another object. It was a conscious design decision made by C++ designers. If you look at it implementation-wise, it is not a problem to get it done. When the user asks the reference to point to a different object, the compiler uses the same "address of variable" logic, and will populate the reference variable with the new variable's address. So, there are no implementation difficulties here. So, why was it not allowed then ?

One main reason I could think of was that pointers already do the same job. So, there is no point in replicating the same set of operations in a reference. Given that we're coming with something new, is there a possibility of giving a new guarantee that a pointer cannot do ? That way we could also avoid the duplication, and get a new feature. So, the concept of "a reference is always valid" was introduced. Clearly, a pointer doesn't guarantee that, since it allows NULL pointers. What that means is that, the lifetime of a reference is a subset of the lifetime of an object. In short, it can be termed as an "alias" to the object. This concept of "alias" gives a high level abstraction to an object. High level abstractions are the need for solving bigger problems, something which programmers can exploit while designing, and something which C++ normally specializes in !


1 comment:

  1. > int &b = &a;

    Do you mean:
    > int *b = &a;

    ReplyDelete