Tip of the Week #232: When to Use auto for Variable Declarations

Originally posted as TotW #232 on June 20, 2024

By Kenji Inoue and Michael Diamond, Google Engineer

Updated 2024-09-30

Quicklink: abseil.io/tips/232

The style guide says in the Type Deduction (including auto) section:

Use type deduction only if it makes the code clearer to readers who aren’t familiar with the project, or if it makes the code safer. Do not use it merely to avoid the inconvenience of writing an explicit type.

Ironically, overuse of auto often leads to code becoming less clear. Over time, however, several patterns have emerged where using auto can improve code clarity and safety, such as:

  • In situations where specifying the type correctly can be difficult and specifying the wrong type can lead to performance or correctness issues, e.g., range-based for loops over a map.
  • In situations where the type information is truly redundant and specification of the full type is distracting, e.g., commonly-used templated factory functions and some iterator uses.
  • In generic code where the type itself is not important as long as it is syntactically correct.

We’ll discuss each of those cases below, with an eye toward clarifying the cases in which auto makes code safer or clearer.

Range-Based For Loops Over a Map

The following code has a problem that each element in the map is unintentionally copied:

absl::flat_hash_map<std::string, DogBreed> dog_breeds_by_name = ...;
// `name_and_breed` is copy-constructed for each element of the map.
for (const std::pair<std::string, DogBreed>& name_and_breed :
     dog_breeds_by_name) {
  ...
}

The unintended copy happens because the value_type of associative containers is std::pair<const Key, Value> and std::pair allows implicit conversions between pair objects if their underlying types can be implicitly converted. Because std::pair::first_type here is std::string and the map entry here has std::pair::first_type of const std::string, the pairs are not the same type and an implicit conversion occurs, copying the contents of the pair despite name_and_breed being declared as a reference.

Using auto, possibly in conjunction with structured bindings (Tip #169), can make the code safer and more performant:

absl::flat_hash_map<std::string, DogBreed> dog_breeds_by_name = ...;

// `auto` with structured bindings - if the element types are clear from local
// context.
for (const auto& [name, breed] : dog_breeds_by_name) {
  ...
}

Sometimes, the element types are not obvious from local context. In that case, you can do this:

// `auto` without structured bindings - allows specifying the element types.
for (const auto& name_and_breed : dog_breeds_by_name) {
  const std::string& name = name_and_breed.first;
  const DogBreed& breed = name_and_breed.second;
  ...
}

Iterators

The names of iterator types are verbose and often provide redundant type information when the type of the container is visible nearby.

Here is an example code snippet that assigns an iterator to a local variable.

std::vector<std::string> names = ...;
std::vector<std::string>::iterator name_it = names.begin();
while (name_it != names.end()) {
  ...
}

All containers expose begin() and end() functions which return iterators, and these iterators have type ContainerType::iterator or ContainerType::const_iterator.

When the type of the container is visible nearby, calling out these types would only have a small benefit of differentiating iterator and const_iterator because the container type part (e.g., std::vector<std::string>) is the same as that of the container. In this case, we can use auto to remove redundancy without hiding helpful information:

std::vector<std::string> names = ...;
auto name_it = names.begin();
while (name_it != names.end()) {
  ...
}

When the container type is not visible locally, prefer to spell out the full iterator type or element type:

std::vector<std::string>::iterator name_it = names_.begin();
while (name_it != names_.end()) {
  ...
}
auto name_it = names_.begin();
while (name_it != names_.end()) {
  const std::string& name = *name_it;
  ...
}

std::make_unique and Other Google-wide Factory Functions

In the following code snippet, std::make_unique and proto2::MakeArenaSafeUnique specify the types to be instantiated.

std::unique_ptr<MyFavoriteType> my_type =
    std::make_unique<MyFavoriteType>(...);

proto2::ArenaSafeUniquePtr<MyFavoriteProto> my_proto =
    proto2::MakeArenaSafeUnique<MyFavoriteProto>(arena);

It is widely known throughout Google that std::make_unique<T> returns std::unique_ptr<T> and proto2::MakeArenaSafeUnique<T> returns proto2::ArenaSafeUniquePtr<T>. In particular, the important part of the resulting type T is specified on the right-hand side (RHS) expression, and it is company-wide knowledge rather than project-specific knowledge. We can use auto here to remove redundancy without hiding helpful information:

auto my_type = std::make_unique<MyFavoriteType>(...);

auto my_proto = proto2::MakeArenaSafeUnique<MyFavoriteProto>(arena);

Generic Code

In some circumstances when writing generic code, such as templates or GoogleTest matchers, the type may be impossible or very difficult to specify (e.g., a type written with template metaprogramming or decltype). In these cases auto may also be appropriate. However, these situations should be rare.

Otherwise: Avoid Using auto

While it can be tempting to use auto in situations where the type is long and seems obvious to you, remember that future readers of the code may not be familiar with your project and the types it uses (why). For example, consider a common pattern of nested proto access.

// Of course `breed` has type `const DetailedDomesticCatBreed&`!
const auto& breed = cat.pedigree().detailed_breed();

auto may also hide basic semantics like constness, whether a type is a pointer, and whether a copy is being made (Tip #44).

// Did the author mean to make a copy here?
// It is not obvious to all readers that `breed` is not a reference even though
// `detailed_breed()` returns a reference!
auto breed = cat.pedigree().detailed_breed();
// Type and semantics are clear.
const DetailedDomesticCatBreed& breed = cat.pedigree().detailed_breed();

Summary of Recommendations

  • Use auto when manually writing out a more specific type would incur a high risk of correctness or performance problems.
  • Use auto to remove redundancy without hiding helpful information when the useful type information is visible locally.
  • For some generic code where the type is impossible or very difficult to specify, auto may be appropriate; these situations should be rare.
  • Avoid using auto in other situations: while it may make it easier for you to write the code or allow you to avoid a line-break, it probably makes the code harder to understand for someone unfamiliar with your project.

See Also

  • https://google.github.io/styleguide/cppguide.html#auto for the authoritative guidance
  • Tip #4: Tip of the Week #4: Automatic for the People
  • Tip #44: Tip of the Week #44: Qualifying auto

Subscribe to the Abseil Blog