Hello again!
In this video, we are going to look at forwarding references.
First, we are going to have a slight detour and talk about nested references,
what happens with those.
We cannot actually declare something as a nested reference, because the syntax does not exist.
The compiler will not let us.
However, it is possible for these to arise internally in the compiler, and this can happen with a type
alias, or a template parameter.
Let's look at the type alias first, because that is a bit easier to understand.
We have a type alias here, for a reference to int.
So whenever we put "int_ref", the compiler will replace this by reference to int.
So we can create a variable which is of type "int_ref" and bind it to an inch, and that will just be
a reference to int.
So that is the type of "j".
We can also declare something as a reference to an "int_ref".
So in this case, "rj" is a reference to something which is a reference to int.
So what happens here?
The compile will perform something called "reference collapsing".
So these multiple references will be collapsed into a single reference.
So "rj" is actually just a reference to int.
So let's try this out.
So here are the various variables that we created on the slide.
Actually, let's first of all check that we cannot create a reference to a reference.
So there we are. "Reference to reference is illegal".
So we have this "rj", which is a reference to an int_ref.
So it is a reference to a reference to an int.
The compiler will collapse this down, to a single reference to an inch.
And to check this, we have a function which takes a reference to int as argument.
So if this has been collapsed, then we should be able to call this function.
On the other hand, if "rj" is not a reference to int, then it will not compile.
And there we are, it does work.
So the compiler has collapsed the reference.
That was all C++98,
so we just had to worry about lvalue references in those days.
With C++11, we also have rvalue references.
The rules for reference collapsing are a bit more complex.
They are basically an "and" operation.
If we have an rvalue reference to something which is an rvalue reference, then that collapses
down into an rvalue reference.
If we have an lvalue reference anywhere, then that collapses down to an lvalue reference.
So, lvalue reference to lvalue reference, rvalue reference to lvalue reference, rvalue reference to
lvalue reference, all collapse down to lvalue reference to int.
And if we have an rvalue reference to rvalue reference, only then does it collapse down to rvalue
reference.
Well, now we can finish digressing and we can get back on to looking at forwarding references. When
we have had an argument followed by a double ampersand,
so far it has always been a specific type.
So if we have something like this, x is a rvalue reference to an object of type Test. Some class.
It can only be bound to an on value.
So we must either pass an xvalue or a prvalue when we call this function.
If we are using the C++17 terminology.
If we have a template parameter followed by a double ampersand, then that is completely different.
This is known as a forwarding reference or sometimes a universal reference, and this parameter x can be bound
to either an rvalue or an lvalue.
So that's why it is called universal.
It can bind to anything.
So if we call this function, the compiler has to instantiate it.
The compiler has to decide what to type to use for the template parameter T, and also the type of the
argument parameter x.
So how does that work?
The compiler will look at the object that is being passed.
Is this an lvalue or an rvalue?
And then it will make the decision.
If the passed object is an lvalue, then the template parameter "T" will be deduced as an lvalue reference
to that type.
So T will be lvalue reference to Test.
And then x is now an rvalue reference to T.
So, that is an rvalue reference to an lvalue reference to test.
Then reference collapsing will occur, and that will collapse down to an lvalue reference to Test.
So X will be an lvalue reference, even if we just passed an lvalue and not a reference in the actual object
that we passed.
If we pass an rvalue object, then the template parameter will be the actual type of the object,
not a reference.
X will be an rvalue reference to that, which is just a normal object.
No reference, so there is new reference collapsing.
And then the result will be an rvalue reference to Test.
So let's go through this in the compiler.
We have our class "Test", which does not need to do anything for this example.
We have our function which takes a forwarding reference as argument, and then we have a main() function
where we call this function with various objects.
First of all, we call this with a normal variable, an lvalue.
The template parameter type will be lvalue reference to Test, and then x will be an rvalue reference
to this. The rvalue reference to lvalue reference will collapse down, to lvalue reference to Test.
So the compiler will instantiate a function which takes an lvalue reference as argument.
If we pass an lvalue object, then it works exactly the same.
We have template parameter type is lvalue reference to Test, x is in rvalue reference to that, and the result
is lvalue to test.
So it will instantiate the same function again.
If we pass an rvalue object, then the template parameter is a Test, not a reference, just a Test.
And x will be an rvalue reference to that.
So that is rvalue reference to Test.
So if we pass in rvalue object, then there is no reference collapsing. The compiler
will instantiate a function which takes an rvalue reference to Test as its argument.
So this is all a bit technical. Is it actually useful? If you remember the last video, we were looking
at the most efficient way to pass an object without modifying it, both by lvalue and by rvalue.
And we decided that the best way is to have two overloads: one which takes an lvalue reference,
and one which takes an rvalue reference.
But the actual function bodies are exactly the same.
So apart from the argument type, these functions are identical.
So, two functions is a bit of a nuisance, but maybe you can live without that.
Say, for example, you have four arguments, and everyone of them can be either an lvalue or an rvalue.
So I think that's 20 different overloads that you'd have to write, with
all exactly the same function bodies, but different arguments.
And then you get things like default arguments or functions with variable numbers of arguments.
And you can see this could easily turn into a nightmare.
If instead you write a function which takes a forwarding reference, then the compiler will instantiate
the correct argument types, depending on what objects we pass.
And all the hard work is done for us, which is always nice. If you can get the compiler to do the work
for you, instead of having to write the code yourself,
that is always a good thing. And that is actually a direction that C++ is going in.
Okay, so that is it for this feature.
I will see you next time.
But until then, keep coding!