decltype and auto Considered

I’ve had the fortune to try out some C++11 features in my day job. Having other programmers start to use some of them in addition to the use-cases being on an actual developing project has been great. Amoung the features I was excited to put to use were auto and decltype.

I’ll look at decltype first since it is less contentious than auto. decltype almost always gives what an entry-level C++ programmer would expect. It always gives what the seasoned programmer would expect. It really only has one potentially unexpected outcome to the entry-level programmer: an lvalue expression’s type is a reference type.

So were x a local variable, decltype(x) differs from decltype((x)). The former being the type of x, the latter being a reference to the type of x. One place where this matters is for functions with decltype deduced return types wherein some programmers may be accustomed to wrapping their return values in parentheses:

decltype(auto) f()
int ret;
// calculate ret
return (ret); // oh no! lvalue expression makes the return type an int&

The function inadvertently returns a reference to a temporary object now!

I think this is a fairly minor quibble though. The benefits of decltype for generic programming clearly outweigh this minor source of confusion.

auto, on the other hand I think is more dangerous. However not for the reasons which seem to be most loudly trumpeted on the blogs and forums I read.

If I may try summarize auto’s deduction in C++ laymans terms: it deduces the simplest mutable form for storing a copy of an object. This is fairly straight forward and the only potentially confusing deductions will, unlike decltype, be highly unlikely for the average programmer to stumble upon.

So what makes auto dangerous?

Hiding the type, in my experience, is very rarely an issue. Most deductions are known by the IDE (and made available to the programmer as code is written). In the infrequent situation that I need to see the variable declaration, the deduction of auto is usually apparent to me from the code it is given to deduce (usually a function call like .begin() or initialization from static values).

Hiding a copy operation doesn’t factor because I understand that if I need a reference, I have to add it (eg: auto&). This is much like I understand that in a pre-auto world I still need to make the type a reference when I want one. If I really have to mimic the exact type in a generic way then decltype comes to the rescue. Furthermore (N)RVO still applies as usual.

In fact I’d go as far as to say that for the longest time the almost-always-auto rule went a long way to writing less dependant code and I was becoming a big proponent of it.

Until I tried to squeeze in a proxy object, that is. Suddenly code written in the pre-auto style would have worked:

MyObject obj = storage.GenericQuery(); // might return proxy object convertible to MyObject
// ... change object
AnotherSystem.AcceptNewObject( obj );

However when this code is written with auto it looks correct:

auto obj = storage.GenericQuery();
// ... change object
AnotherSystem.AcceptNewObject( obj );

Now the code has introduced an additional requirement: that the object returned by the query may not be a proxy object which modifies the original! This introduces a very hard to track down bug because on first blush the more generic version of the code using auto would appear to do the same thing as it might have pre-auto. However this could not be farther from the truth.

In a pre-auto world the proxy object is free to use references or pointers internally which allow it to behave much like an l-value reference to another object (without actually having a reference on its type). The caller would specifically state the storage type of obj to be some non-proxy type and the proxy object would presumably have an implicit conversion to correctly construct a copy of the object it represents.

In the post-auto world the more generic implementation has subtly introduced an implicit requirement. It requires that the type returned from GenericQuery() can only be a copy of the object or a reference to the original object. In both cases auto would correctly deduce the storage type to something semantically appropriate. However in the case of the proxy object, auto would create a copy of the proxy object to be worked on. So in actual fact the example function would attempt to make changes to the original object via the proxy instead of the copy.

Furthermore capturing a proxy type intended to be stored as another can lead to further frustration if that type is then passed on to other template’d types with specializations dependent on the expected storage type not the proxy type.

Upon some consideration I really wish classes could define what their deduced storage type should be. However this would not only make auto confusing (now a programmer would have to look up a type declaration to see what auto will deduce) but also might have knock-on effects for template type deduction rules.

Unfortunately the only solution that came to mind is to define a storage_type() function which does any transformation necessary to turn an object into a type suitable for storage. The above example code would then be:

auto obj = storage_type( storage.GenericQuery() ); // no transformation needed if the object returned is not a proxy object
// ... change object
AnotherSystem.AcceptNewObject( obj );

However this method has it’s own raft of potential issues based on visibility of the storage_type function. While these can be mitigated, that’s a separate discussion altogether.

I think this is a game-changer for me. auto now requires extra care and consideration when I am writing code. Clearly auto is needed to capture lambda types and still saves headache when a type is a highly specialized template with lots of noisy cruft. However its use in generic code has to be more considered rather than less so now.

decltype and auto Considered

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s