Hello again! In this video, we are going to look at the decltype keyword.
You may remember the unique pointer with a custom deleter.
We needed to have a template parameter, which was the same type as a lambda expression.
And then I threw in this decltype, which made everything magically work.
So in this video we are going to find out what this decltype keyword actually does.
The decltype keyword was added in C++11.
It can only be used at compile time.
decltype gives the type of its argument, and this argument must be an expression or an object. Before
C++11,
Some compilers offered this feature using a non-standard extension, "typeof".
There were various implementations, and the one for C++11 was going to be different. So they decided
to give it a different name as well.
So we have to decltype.
decltype does not evaluate its argument, and it does not cause any code to be executed. Either at
run-time or compile time.
All that happens, is that the compiler will replace the decltype with whatever type the argument
would return.
For example, if we have decltype 1 + 2, then the compiler will see that 1 + 2
will return something which is an int, and it will replace "decltype(1 + 2)" by int.
And then we have int y.
So this will declare y as a variable, which has the same type as 1 + 2.
If the argument is a named variable, then decltype will be replaced by the type that this argument
was declared as.
So we have here a variable with the name "x", declared as type int, so "decltype(x)" will be replaced by
int, which was the type that x was declared as.
You may think this is rather similar to "auto", and in some ways it is.
We mostly use "auto" when we are declaring a variable, and we want this variable to have the same type as the
initializer.
So here we have a variable "x" which will have the same type as 6, and this will have the initial value
6.
So we are declaring "x" as an int, with initial value 6.
decltype is used with an existing object, or with an expression, and it is replaced by the type of that
expression.
So here we have "decltype(x)".
So this will be replaced by the type of "x", which is int.
So we have a variable "y", which is type int.
There is another difference, which is that decltype will retain the qualifiers.
So if we have a const or reference, then "auto" will drop those.
So "auto y". In this case, "y" will just be an int, not a const int. "decltype(cx)" will be replaced by
the type of "cx", which is const int.
So, "cz" will be a const.
So let's try this out.
We have "x", which is a const int.
Then we have "y", which is declared as "auto".
So "y" will have the same type as int,
but without the const. Then we have "z", which is declared as "decltype(x)".
So this will have the same type that "x" was declared as.
So this will include the const.
So we can increment "y".
And that has incremented "y".
But if we try to increment "z", which was declared as "decltype(x)", then we cannot increment that.
Because "z" is a constant.
The rules for decltype.
They may seem a bit complicated, but in effect, decltype will do what you expect. If you have
decltype with a named variable.
We have already mentioned that. You get the declared type.
If we have decltype with an argument which is an lvalue expression, then the result will be an
lvalue reference to the deduced type of the expression.
So, if "x" and "y" are both ints here, then "decltype(x + y)" will give reference to int.
If we put a variable inside a pair of brackets, then it becomes an expression.
So decltype of "x" inside brackets, will be a reference to int.
For rvalues. If we have a pure rvalue,
So that is a literal, then this will be replaced by the type of the literal.
So "decltype(2)" will be an int. If we have an xvalue, an "expiring" value.
So that is a temporary value. Then the compiler will deduce the type of the argument, and replace it
by an rvalue reference to that type.
So if we have a temporary object of class Test, then this will be replaced by an rvalue reference
to a Test.
So, decltype of a variable will be the type of the variable.
So "y" will be int.
decltype of an lvalue expression will be an lvalue reference.
So "p" will be an lvalue reference to int. Then with a pure rvalue, "z" will be an int. And with
a temporary object, "t" will be rvalue reference to a Test.
In C++14, we can use "auto" as the argument to decltype, which seems rather a weird thing to do.
The idea of this is to have something which works exactly like "auto", but without dropping the qualifiers.
So if we have "auto b", initialized from "a", then "b" is just going to be an int.
If we use "decltype(auto)" then this will keep the const and the ref, and "c" will be a constant reference
to int.
So we have the same thing again, but this time using "decltype(auto)". So we can increment "b", because
"b" is not const.
But "c" is a const ref, so we cannot increment it.
And finally, when would you use this?
Well, decltype is mainly useful in compile-time
programming.
The classic example is for the return type of a template function.
In C++11, you can have a function which returns auto. But you have to give the return type, which
rather seems to defeat the point of returning auto.
But you can use decltype here.
So, if you have a template function which returns the sum of its arguments, what will the return type
be?
If the arguments have the same type, then it is obvious. The return type will be the type of the arguments.
If the arguments have different types, then it may not be. If an argument is an int and another
argument is double, then the return type needs to be double.
If you have a character pointer and an int, then the return value needs to be a character pointer.
So with decltype, this is not a problem.
You can ask the compiler, what will be the return value from adding these? And then the compiler will
replace this by the returned type.
So when this is instantiated, the return type of this function template will be the return type for
adding the int and double, or the character pointer
and int.
You can also use decltype with the generic lambdas in C++14.
So these are lambdas which have arguments of type "auto".
And then, in the lambda body, you could have variables which are of this type. As a rather silly example,
you can return a vector of elements which have the same type as the parameter. I mean, you could just call
the vector constructor directly. But it is an example! And then we use the "auto" return value as well.
So the company will deduce the return type.
So here is our template function.
The return type will be the type
of adding the arguments to the function. Then we have our generic lambda which will return a vector,
whose elements are the same type as its arguments.
And then we call our template function, with arguments 2 and 2. And a C string and and an offset.
And then we call our "make_vector" with the same arguments.
So, adding 2 and 2 gives 5. Adding the C string, "hello"
and 2 gives the C string, with the first two characters missing.
"make_vector" with arguments 2 and 3 gives a vector with 3 elements of value 2, type int.
And doing that with a C string, gives a vector with two elements, which are C strings.
Okay, that is it for this video, and this section, and the language part of the course.
I will see you next time in the project.
But until then, keep coding!