Modern C++ Features – auto for variables

This is my second post of a series on new C++ features, where new means C++11 and up. Keyword `auto` for variable type deduction is one of the most known C++11 features, but proposed uses range wildly. Therefore I will put together what I think are simple guidelines that facilitate its use.

How it works

I will make this section very short and sketchy, because I assume most readers will already know about `auto`. For those who don’t there is much literature and articles on the web with more details.

For variable type deduction, one can use `auto` instead of a type name in a variable definition. The compiler will then deduce the type of the variable from its initializer. To do that, it uses the same mechanisms as in function template argument type deduction, with a little messy exception, when it comes to braced initializers.

The most cited example for `auto` is when you have a hard to spell type name like standard container iterators, but you can also use it with other types:

std::vector<int> numbers;
for (auto iter = std::begin(numbers); iter != std::end(numbers); ++iter) {
  auto& n = *iter;  
  n = someFunction();
}

Here, the type of `iter` is deduced as `std::vector<int>::iterator`, and the type of `n` is `int&`. Note that `n` is explicitly declared to be a reference, else it would be of type `int` and contain simply a copy of its initializer.

Why use it

In the above example there is one obvious benefit: Typing `auto` is much shorter than typing `std::vector<int>::iterator`. In addition, there are even types that are completely unknown, for example those of lambdas. On the other hand, typing `auto&` is longer than `int&`, so why stick to `auto` in that case?

There are two more reasons to use `auto` beyond less typing: The first is consistency. If you use `auto` in some places, where you want a variable to have the type of its initializer, you should uses it in every such case. Having two different styles for the same case makes readers of your code wonder why you use one and not the other, and where is the difference, so it hurts readability – not much, but more than necessary.

Another reason is that of changeability. The types in that example are all deduced from the type of the initial variable `numbers`, which obviously is a collection of numbers. What if someone finds `std::vector` to be not suitable for the task? Or, more likely if the numbers should not be `int`s but something else, maybe `unsigned long`s or `double`s?

One can simply change the type of the container to e.g. `std::array<unsigned long>`, and everything else falls into place automagically – `iter` becomes a `std::array<unsigned long>::iterator` and `n` a `unsigned long&`. If it had been a `int&` explicitly, we would have to change it manually.

How to use it

There are several candidate notation one could think of at first when it comes to `auto`, but only one is right. Direct initialization as we know it does not work well with `auto`, neither the old style nor with uniform initialization:

  • `auto x(5)` will work, but if you have e.g. a type `Foo` and try `auto x(Foo())` you get the surprising result that this actually is a declaration of a function that has its return type automatically deduced – this is another use of `auto` that I will cover in a later post.
  • `auto x{something};` will deduce the wrong type, at least in C++11 and C++14 – it is of type `initializer_list<Sometype>`, where `Sometype` is the type of `something`.

So, only copy initialization works, i.e. always use `auto x = something;`. In addition, the initializer should not be in curly braces, i.e. `auto x = {something};` – this will also give a `initializer_list<Sometype>`.

With auto, always use copy initialization without braced initializers.

When using `auto`, good function and variable names become yet more important. With an explicit type for a variable, a reader can always look it up if the function is reasonably short.

Without that, bad names can be a great hindrance. `auto x = foo();` tells us that the return type of `foo` and the type of `x` are the same, but we don’t have the slightest idea what it is. On the other hand, `auto points = calculateScore();` tells us that we probably have a numeric type and even some semantics, while the syntax is the same.

When to use it

When to use `auto` should be pretty obvious by now:

Use `auto` whenever the type of a variable is dependent on other code and the types used there.

Fixed types

What about when we want to fix the type of a variable? There are two ways to do that: Either state the type of the variable explicitly, or state the type of the initializer explicitly:

std::size_t size{2}; //2 is int, but we want size_t
auto size = std::size_t{2}; //same

There are arguments for both variants:

Explicitness

The first style without `auto` might be a bit more explicit for the reader, since the fixed type is the first thing he sees. With `auto`, you have to read over the variable name to the start of the initializer to see the fixed type.

On the other hand, even if the implementer fixes the type, it is often of secondary importance for the reader to know the exact type, and, as always, the name of a variable should give enough information about what is in it.

In addition, if an explicit C++ cast is used, e.g. `dynamic_cast<Derived*>(basePtr)` the type is already stated in the cast and can not be omitted, which is another point for using `auto` in that case.

Enforcement of Initialization

`auto` enforces the explicit initialization of a variable, which is good style. It is not possible to forget the initializer, because the code simply can not compiler without it. However, since compilers usually warn about use of uninitialized variables and static analyzers tend to be pedantic about the topic too, I consider this only a minor argument.

Noncopyable types

Copy initialization is not possible for types that have no move or copy constructor. Direct initialization with auto is not possible for them either, because you have no way to state the type without calling a move/copy constructor. That means you can not possibly instantiate objects of such types with `auto`.

As you can see, neither variant is completely perfect, so this is a point where I won’t give a general guideline except to be consistent in your codebase. However, if you want to know what one of the great C++ gurus thinks about the topic, read Herb Sutter’s article titled “Almost Always Auto”.

Previous Post
Next Post

Leave a Reply

Your email address will not be published. Required fields are marked *