• entries
146
• comments
436
• views
197185

# The New C++, Part 2 - decltype

2085 views

Working with templates is annoying. Working with return types deduced from template arguments is even more annoying. Things quickly tend to spiral out of control, frequently requiring the introduction of typedef's just to make the code marginally more readable. More importantly though, its very difficult to get the TYPE of an expression out of an expression without deducing it statically. This isn't actually much of a problem with the introduction of auto. The problem comes in, though, when you wish to declare the return type of a function dependent upon the result of an expression.

# Motivational Studies

This provided one of the main motivating factors for the introduction of the decltype operator in C++11. Another reason for the existence of decltype is for the purposes of providing perfect forwarding. Something that is a necessity for rvalue-references, and move semantics. It is also useful though in other ways. A very trivial example of the problems with forwarding without decltype is demonstrated as follows:
int foo(int const& i) { return i; }float const& foo(float const& f) { return f; }templateT do_something(T const& t) { return foo(t);}
The problem here is simple, do_something simply isn't forwarding the return type properly. For the float version of foo, do_something should be returning a constant reference to a float, whereas it simply returns a float. You cannot simply solve this by changing the return to a constant reference either, because then you're returning a reference to an unnamed temporary when you call the integer version of foo.

The question thus becomes: How can we write a do_something such that it returns the exact same type as the function it forwards to? Well, to start off with, we need to change up the format of the function a wee bit. The biggest issue we have at the moment is that the return value is clearly going to be determined by the invocation of foo against the parameter passed to do_something, however at the point that the return type is specified we do not even yet KNOW of the parameter to do_something:
templateT const& do_something(T const& t) {^ ^--- We do not know about this|-----------------At this point

So we clearly need some way to re-arrange it so that we can know about things like the parameters to the function. Here enters the new result type specification, or as the standard calls it, late-specified return type...
templateUNKNOWN const& do_something(T const& t) -> T {
clearly though we're still missing something, as we need to specify a placeholder for the return type. Thankfully we have such a something, its called auto.

templateauto const& do_something(T const& t) -> T {
Ok, so we've gone to all this work of re-arranging our return type so that we can know about the function parameters... we still don't yet have a way to figure out what the return type should actually be, and now we bring in decltype.

The decltype operator returns the exact type of an expression. Its important to understand that the type returned is a fully qualified type, down to cv-qualifiers. Much like the sizeof operator, the expression passed to the decltype operator is not evaluated except statically to determine its resulting type. This very useful, because you can pass parameters to get the type you require out of an expression without having to worry about ensuring that the parameters are actually valid. An example would be passing in null pointers to a function which expects non-null pointers. Thus we can now redefine do_something as such:
templateauto do_something(T const& t) -> decltype(foo(t)) { return foo(t);}
Which now gives the appropriate perfect forwarding, as expected. More importantly, functions that return references or constant references will appropriately forward their return types.

# Building up to a Cleaner Algorithm

Given what we know now about decltype we can go back to our algorithm from the previous post. You may remember we had just finished cleaning it up, and it looked much like so:
templateauto find_first_pair(Sequence1 const& seq1, Sequence2 const& seq2, MatchBinaryFunctor match) -> std::pair{ for(auto itor1 = seq1.begin(); itor1 != seq1.end(); ++itor1) { for(auto itor2 = seq2.begin(); itor2 != seq2.end(); ++itor2) { if(match(*itor1, *itor2)) { return std::make_pair(itor1, itor2); } } } return std::make_pair(seq1.end(), seq2.end());}
Auto has clearly made a big difference, and it is much easier to read, however we still have the small problem of that return type, which is ridiculously long and rather hard to actually read. Not to mention, we're hard coding the iterator type we're returning, something that's usually not very useful. With the trivial application of decltype we can eliminate a large chunk of unreadable code by reducing it down to a simple expression:
templateauto find_first_pair(Sequence1 const& seq1, Sequence2 const& seq2, MatchBinaryFunctor match) -> decltype(std::make_pair(seq1.end(), seq2.end())){ for(auto itor1 = seq1.begin(); itor1 != seq1.end(); ++itor1) { for(auto itor2 = seq2.begin(); itor2 != seq2.end(); ++itor2) { if(match(*itor1, *itor2)) { return std::make_pair(itor1, itor2); } } } return std::make_pair(seq1.end(), seq2.end());}

It is important to note that decltype is not just restricted to return types. In fact, decltype can be used to declare variables, although it generally makes more sense to use auto... unless you need the exact result type of the expression. More on that when we get to rvalue-references though.

## Recommended Comments

There are no comments to display.

## Create an account or sign in to comment

You need to be a member in order to leave a comment

## Create an account

Sign up for a new account in our community. It's easy!

Register a new account

## Sign in

Already have an account? Sign in here.

Sign In Now

• 9
• 11
• 21
• 26
• 17