Example of C++ Template Magic

10 Second Overview of r-value vs l-value References

The original definition of r-values and l-values from C is as follows: An lvalue is an expression that may occur on the left or on the right side of an assignment, but an rvalue can only appear on the right hand side. For example:

inta=42intb=43// a and b are both l-values:
a=b;// ok
b=a;// ok
a=a*b;// ok
// a * b is an r-value:
intc=a*b;// ok, r-value on right side
a*b=42;// error, r-value on left side

In C++, however, there are some subtleties. Those can be avoided by substituting the following definition: An lvalue is an expression that refers to a memory location and we can take the address of it via the & operator. An rvalue is an expression that is not an lvalue. Examples:

inti=42i=43;// ok, i is an l-value
int*p=&i;// ok, i is an l-value
int&foo();foo()=42;// ok, foo() is an l-value
int*p1=&foo();// ok, foo() is an l-value
intfoobar();intj=0;j=foobar();// ok, foobar() is an r-value
int*p2=&foobar();// error, cannot take the address of an r-value
j=42;// ok, 42 is an r-value

I strongly recommend thoroughly understanding these examples before proceeding. This is a good point to continue reading if you want a more thorough explanation.

Additionally, this article gives a great overview of reading C declarations in case you feel rusty.

The Code We’re Going to Analyze

This code is a declaration found in aos/common/control_loops/control_loop.h. It is one of four helpful typedef statements at the top of the file to allow the aos::controls::ControlLoop to understand and use the types of the goal, status, position, and output queues in method declarations.

static_cast to MakeMessage()

In analyzing this code, we’re going to start from the inside out. We’re also only thinking about types, and not results, which is important to note.

Starting out, we see static_cast<T *>(NULL). This will construct a null pointer of type T, which is the queue group type passed to ControlLoop as the template parameter. We are allowed to make certain assumptions about what T contains because we pass it in. If we get that wrong, there will be a compile error.

Anyways, we have a pointer of type T. And we use the -> operator to access it’s variable named goal. This represents the goal queue as defined in the relevant control loop queue file (see y2018/control_loops/superstructure/superstructure.q). This queue has a method called MakeMessage(), which will return a ScopedMessagePtr<message type> which is basically a fancy pointer wrapper around the message type the queue handles. .get() will then return the raw pointer, which would be of type pointer to the type of the message handled by the queue.

In conclusion: *(static_cast<T *>(NULL)->goal.MakeMessage().get()) will give us something of the type of the messages that the goal queue handles. Note, of course, that this would never evaluate because you would be trying to de-reference a null pointer. NULL is typically defined to 0, and normally 0 is outside of the range of memory that a program can access, so the kernel will get angry, and the process will fail with a ‘Segmentation Fault’.

The Magic of decltype

decltype is what enables a lot of amazing C++ template magic. It let’s you get the type that an expression would evaluate to without actually evaluating the expression. For example, let’s say I have the following for given template types U and T.

The magic at work here is that we can substitute the type of c for the decltype-found type of the addition operation. C++14 enabled the auto function return type, which allows this to actually be useful as follows:

auto in this case is just fancy syntax telling C++ to wait for the -> part. This is because prior to the parameter declaration, a and b didn’t exist so it wouldn’t resolve. The method written like this will have the return type of the sum of a + b, but without actually evaluating that expression.

This is the magic that allows us to look like we’re deferencing a null pointer when in fact decltype is simply pretending that we could do that, and finding the type.

Thus, decltype(*(static_cast<T *>(NULL)->goal.MakeMessage().get())) will give the type returned by dereferencing the return type .get(), which will in the end result in the type processed by the queue. Note that this isn’t a variable of that type, it is actually the type.

remove_reference and a Possible Implementation

The point of remove_reference is that it, well, removes the reference part of a type. The ::type part will make more sense once we go over a reference implementation.

typedef lets you alias a type. For instance, typedef int this_is_an_int would let us do stuff like this_is_an_int c = 46. To the compiler, this_is_an_int ends up looking just like an int. Whenever we wrote something like remove_reference<int>, the compiler would define a struct that looks as follows:

structremove_reference<int>{typedefinttype;};

Therefore, if we did remove_reference<int &>::type, there would be a struct as follows:

structremove_reference<int&>{typedefinttype;};

And ::type would be int, which is int & with the reference part removed. This also works for r-value references like int &&.

As you can imagine, std::remove_reference<decltype(*(static_cast<T *>(NULL)->goal.MakeMessage().get()))>::type will give you the same type as decltype gave us, but now not a reference if it was previously a reference.

Finally, typedef lets us alias the type we discovered to GoalType, so we don’t have to type out that giant blob every time we need to deal with a message. This let’s us declare RunIteration as: