Skip to content

Joseph Mansfield

Don't use auto unless you mean it

Since it's repurposing in C++11, the auto keyword has largely been used as a convenience. It's one of the more well-known new features, perhaps because many programmers saw it as a way to simplify their code, but also has little consensus on when it should be used. It provides automatic type deduction in declarations based on their initializers. For example, auto x = 5; will deduce that x is an int.

To beginner C++ programmers, auto may look like it provides dynamic typing as in JavaScript or Python. This is not the case. The type is still completely static because the type of the initializer is static too. Even if it were a declaration inside a function template, in which the initializer could have a different type depending on the particular template arguments, the type is static for each template instantiation and those template instantiations are themselves static. C++ is still a statically typed language.

For many, auto is seen as a way to increase the brevity of code and hide away ugly C-style declarations, with the intention of making it easier to read and faster to write. After all, auto can save us from typing out the hideously long types we often find in C++. Some suggest to use auto as much as possible. I strongly feel that hiding away types is a stylistic choice and therefore the responsibility of the IDE and not of type deduction. Also, this is a difficult convention to stick to because it's hard to agree on whether a particular use is acceptable or not. Consider the following:

// Assuming range-based std::max_element
auto best_candidate = std::max_element(employees, LessSuitableForTask{task});

Is it clear what we can do with best_candidate? Is it the maximum value or an iterator pointing at the maximum value? If it's an iterator, what kind of iterator? For somebody who is fairly comfortable with C++, perhaps this might be obvious. For anybody else, we'd have to look up the definition of std::max_element just to find out what its return type is and then figure out what that type represents. This convention is therefore a matter of deciding how proficient a person needs to be to understand your code. If the initializer used functions and types from some other library, the reader has to be proficient with those also. When using auto, you are arbitrarily making the choice of whether it is acceptable or not. Additionally, if the initializer changes to give a type that is no longer suitable, you'll get errors from further down the code. This doesn't sound great to me.

Instead of using auto so much, my proposed convention is as follows: choose types that impose the minimum requirements on a declaration that are necessary for the code in which it is used to behave correctly. Under this convention, auto imposes no requirements. It says “This variable can have any type and the code in which it is used will run just fine regardless.” This gives auto some meaningful semantics. You may correctly observe that this means we have only two choices: no requirements with auto, or no leniency with the actual type itself. That is, for now.

A proposal to introduce concepts to C++ is in the works (N4040 is the latest draft at the time of writing). Concepts would allow us to place requirements (called constraints) on template arguments. At the moment, a template argument (like typename T) will accept any type, which results in horrible errors being thrown from the internals of the template if that type is not appropriate. Go ahead; try doing something like std::copy(5, 10, “foo”) and enjoy the mess. Worse, it could compile fine and behave erratically. With concepts we'll be able to specify requirements on the arguments:

template <InputIterator InputIt, OutputIterator OutputIt>
OutputIt copy(InputIt first, InputIt last, OutputIt d_first);

In fact, a terse syntax is also proposed which will allow us to use concept names in the argument declarations themselves:

auto copy(InputIterator first, InputIterator last, OutputIterator d_first)
  -> OutputIterator;

So with concepts, saying that a parameter is an InputIterator says that the arguments should meet the requirements imposed by the InputIterator concept. If you pass something that doesn't, you'll get a nice clean error message.

We can also use auto in this terse syntax as the equivalent of typename T with no constraints:

void foo(auto something);

Given the semantics of concepts for arguments, this means “I impose no requirements on the type of something.” Anything will do. This syntax is also being introduced for generic lambdas in C++14:

auto add = [](auto x, auto y) {return x + y;};

It's intended that concepts will eventually be usable in normal variable declarations too, although a proposal has not yet been written. The previous std::max_element example could then be written:

InputIterator best_candidate = std::max_element(employees, LessSuitableForTask{task});

This implies that any use of best_candidate only requires that it meets the requirements of an InputIterator. We are hiding away the actual type of the iterator, but not hiding the information that is actually important. On the other hand, auto hides everything. You can hopefully now see that the convention I have proposed unifies the declarations of both function arguments and variables:

some_type name = /* must be convertible to some_type */;
SomeConcept name = /* must meet SomeConcept requirements */;
MoreGeneralConcept name = /* must meet MoreGeneralConcept requirements */;
auto name = /* can be anything */;

void foo(some_type); // must take something convertible to some_type
void foo(SomeConcept); // must take something that meets SomeConcept requirements
void foo(MoreGeneralConcept); // must take something that meets MoreGeneralConcept requirements
void foo(auto); // can take anything

In both cases, you give the type or concept that has the least requirements required in order to use that object in the way you want to. Under this rule, auto means anything. Likewise, auto& means an lvalue reference to anything, auto&& is a universal reference, and so on.

This convention has multiple additional benefits. In the same way that template arguments allow the arguments to a function to change type, using concepts and auto allows the initializer of a declaration to change. As long as the initializer returns an object that meets the requirements, everything is fine. It also ensures that a nice clean error will be reported if a type is used that doesn't meet the requirements, unlike using auto which has the same problems as templates do today. It also makes the code easier to read than if auto were used for everything because you only need to understand what the type or concept represents. Also, concept names are brief and to the point.

If you see auto under this convention, it means you really, really do not need to care about the variable's type. This means it should be used significantly less than it is being used now. For example, I would not use it in a range-based for loop unless I could really be iterating over anything. Instead, I'd use the type itself:

for (contact_details contact : contacts) {
  send_email(contact.get_email_address(), "Hello, World!");
}

In this example, I'm calling contact_details::get_email_address, which means that contact has to be a contact_details object. This has a number of advantages over using auto:

  1. Finding the type of contact only requires looking at its declaration and not at the type of contacts.
  2. If the container type changes, you'll get an error directly from the for loop declaration, rather than deeper in your code.
  3. It expresses your intent to use contact in ways only a contact_details object could be used.
  4. It's consistent with all other declarations including parameters of templates.

I'd say it's also better than writing decltype(v)::value_type because that ties the declaration to the initializer by assuming that it has a value_type typedef. Really, contact doesn't need to care.

If I were writing a generic loop that could iterate over any kind of object and process it, auto would be fine:

for (auto element : range) {
	  process(element);
}

Since concepts won't be around until C++17 and the ability to use concepts in normal variable declarations may not appear until later, this convention requires a lot of patience. Just don't get too excited about auto and use it everywhere. Instead, use it with some idea of how it will be used in the future. For now, use specific types in most cases. It's better to be overly specific than overly general, because using auto unnecessarily can lead to unclear code and confusing bugs. Using it too much now will make your code less idiomatic in the future. The only downside will be that if you change the type of the initializer, you'll have to change the type of the declaration, but that shouldn't happen too often. Obviously for now we need to be overly general with templates, otherwise they'd be useless, but for declarations we should prefer to be specific.

Herb Sutter has previously proposed AAA Style (Almost Always Auto), which is pretty much the opposite of what I'm suggesting. However, we don't entirely disagree. Many of his arguments actually back up what I'm suggesting, particularly in terms of writing against interfaces instead of implementations. In his append_unique example, he remarks that Container& c makes it clear what the requirements of c are. This is simply mimicking what concepts will provide and is certainly a good practice for template arguments while we don't have them. However, auto doesn't have this benefit at all. Instead, auto is like writing typename T without any information about requirements. It certainly is less readable, perhaps not for the writer of the code, but for anybody else who wants to read and understand it.

One problem that the AAA Style tries to solve, which I do not, is the horrible C declaration grammar. By using auto everywhere, we can easily see that a line is a variable declaration and we don't have to decode its type. Firstly, I don't think this should be the responsibility of auto, which is a much more powerful tool than a var-like keyword. Secondly, we can generally avoid the incredibly ugly type names and concepts will help us to do this.

In summary, auto means anything. Only use it when you mean it, otherwise you make your code harder to read and introduce further problems. Just be patient for concepts, which will make this convention unified and logical. It'll pay off.