Auto Type Deduction in Range-Based For Loops

Have you ever wondered which of the following variants you should use in range-based for loops and when? auto, const auto, auto&, const auto&, auto&&, const auto&&, or decltype(auto)? This post tries to present rules of thumb that you can use in day-to-day coding. As you will see, only four of these variants are generally useful.

Introduction

Consider the following mapping of words into their occurrence count:

std::map<std::string, int> wordCount;

Lets say we want to iterate over the container and modify the occurrence counts. In C++98, we could do this in the following, standard way:

for (std::map<std::string, int>::iterator i = wordCount.begin(),
		e = wordCount.end(); i != e; ++i) {
	// ... word: i->first, count: i->second
}

As you can clearly see, this is lengthy and tedious to write. Moreover, to access the words and their occurrences, we have to use i->first and i->second, whose meaning is not obvious.

Since C++11, we can use a range-based for loop, which provides a more succinct notation:

for (std::pair<const std::string, int>& p : wordCount) {
	// ... word: p.first, count: p.second
}

As a side note, the const before std::string is necessary because keys in maps are immutable. Without the const, the code would fail to compile.

We may simplify the code even further by combining the range-based for loop with automatic type deduction:

for (auto& p : wordCount) {
	// ... word: p.first, count: p.second
}

The present post is dedicated to different forms of automatic type deduction and their suitability for use in day-to-day coding.

However, before we jump into that, let us briefly consider the second disadvantage of the previous approaches. As you can see, there is still a need to use the non-obvious p.first and p.second to access the words and their occurrence counts. Fortunately, in C++1z, we will be able to write this:

for (auto& [word, count] : wordCount) { // C++1z
	// ...
}

It uses so-called structured bindings to decompose the pairs into two components: the word and its occurrence count. As of 2016-08-17, no compiler supports this feature, although the development version of Clang has this implemented.

Automatic Type Deduction

We will go over the following type-deduction variants, one by one:

  • auto
  • const auto
  • auto&
  • const auto&
  • auto&&
  • const auto&&
  • decltype(auto) (since C++14)

As a simplification, we will only consider the const qualifier and ignore volatile. Its use in range-based for loops is seldom, anyway.

What is important to note is that the used type-deduction variant not only provides instructions for the compiler but also signalizes an intent to the reader of your code. For example, when one sees auto&, he or she can assume that the elements of the range will be modified. If they are not modified, the reader may be puzzled and start to ask: Is there a bug in the code? Shouldn’t const auto& be used instead? Is there a hidden reason behind the absence of const? Therefore, when using automatic type deduction, try to choose a variant that makes your intent clear.

auto

for (auto x : range)

This will create a copy of each element in the range. Therefore, use this variant when you want to work with a copy. For example, you may be iterating over a vector of strings and want to convert each string to uppercase and then pass it to a function. By using auto, a copy of each string will be provided for you. You can change it and pass forward.

The following facts need to be kept in mind when using auto:

  • Beware of containers returning proxy objects upon dereferencing of their iterators. Use of auto may lead you to inadvertent changes of elements in the container. For example, consider the following example, which iterates over a vector of bools:
    	std::vector<bool> v{false, false, false};
    	for (auto x : v) {
    		x = true; // Changes the element inside v!
    	}
    	

    After the loop ends, v will contain true, true, true, which is clearly something you would not expect. See this blog post for more details. Here, instead of using auto, it is better to explicitly specify the type (bool). With bool, it will work as expected: the contents of the vector will be left unchanged.

  • Using just auto will not work when iterating over ranges containing move-only types, such as std::unique_ptr. As auto creates a copy of each element in the range, the compilation will fail because move-only types cannot be copied.

const auto

for (const auto x : range)

The use of const auto may suggest that you want to work with an immutable copy of each element. However, when would you want this? Why not use const auto&? Why creating a copy when you will not be able to change it? And, even if you wanted this, from a code-review standpoint, it looks like you forgot to put & after auto. Therefore, I see no reason for using const auto. Use const auto& instead.

auto&

for (auto& x : range)

Use auto& when you want to modify elements in the range in non-generic code. The first part of the previous sentence should be clear as auto& will create references to the original elements in the range. To see why this code should not be used in generic code (e.g. inside templates), take a look at the following function template:

// Sets all elements in the given range to the given value.
template<typename Range, typename Value>
void set_all_to(Range& range, const Value& value) {
	for (auto& x : range) {
		x = value;
	}
}

It will work. Well, most of the time. Until someone tries to use it on the dreaded std::vector<bool>. Then, the example will fail to compile because dereferencing an iterator of std::vector<bool> yields a temporary proxy object, which cannot bind to an lvalue reference (auto&). As we will see shortly, the solution is to use “one more &” when writing generic code.

const auto&

for (const auto& x : range)

Use const auto& when you want read-only access to elements in the range, even in generic code. This is the number one choice for iterating over a range when all you want to is read its elements. No copies are made and the compiler can verify that you indeed do not modify the elements.

Nevertheless, keep in mind that even though you will not be able to modify the elements in the range directly, you may still be able to modify them indirectly. For example, when the elements in the range are smart pointers:

struct Person {
	std::string name;
	// ...
};

std::vector<std::unique_ptr<Person>> v;
// ...
for (const auto& x : v) {
	x->name = "John Doe"; // This will change the name of all people in v.
}

In such situations, you have to pay close attention to what you are doing because the compiler will not help you, even if you write const auto&.

auto&&

for (auto&& x : range)

Use auto&& when you want to modify elements in the range in generic code. To elaborate, auto&& is a forwarding reference, also known as a universal reference. It behaves as follows:

A detailed explanation of forwarding references is outside of scope of the present post. For more details, see this article by Scott Meyers. Anyway, the use of auto&& allows us to write generic loops that can also modify elements of ranges yielding proxy objects, such as our friend (or foe?) std::vector<bool>:

// Sets all elements in the given range to the given value.
// Now working even with std::vector<bool>.
template<typename Range, typename Value>
void set_all_to(Range& range, const Value& value) {
	for (auto&& x : range) { // Notice && instead of &.
		x = value;
	}
}

Now, you may wonder: if auto&& works even in generic code, why should I ever use auto&? As Howard Hinnant puts it, liberate use of auto&& results in so-called confuscated code: code that unnecessarily confuses people. My advice is to use auto& in non-generic code and auto&& only in generic code.

By the way, there was a proposal for C++1z to allow writing just for (x : range), which would be translated into for (auto&& x : range). Such range-based for loops were called terse. However, this proposal was removed from consideration and will not be part of C++.

const auto&&

for (const auto&& x : range)

This variant will bind only to rvalues, which you will not be able to modify or move because of the const. This makes it less than useless. Hence, there is no reason for choosing this variant over const auto&.

decltype(auto)

for (decltype(auto) x : range) // C++14

C++14 introduced decltype(auto). It means: apply automatic type deduction, but use decltype rules. Whereas auto strips down top-level cv qualifiers and references, decltype preserves them.

As is stated in this C++ FAQ, decltype(auto) is primarily useful for deducing the return type of forwarding functions and similar wrappers. However, it is not intended to be a widely used feature beyond that. And indeed, there seems to be no reason for using it in range-based for loops.

Summary

To summarize:

  • Use auto when you want to work with a copy of elements in the range.
  • Use auto& when you want to modify elements in the range in non-generic code.
  • Use auto&& when you want to modify elements in the range in generic code.
  • Use const auto& when you want read-only access to elements in the range (even in generic code).

Other variants are generally less useful.

Further Information

Check out the following sources for more information regarding automatic type deduction and range-based for loops:

Also, if you are aware of any use cases when you would use one of the generally less useful variants, be sure to leave a comment.

Discussions

You can also discuss this post at r/cpp and Hacker News.

7 Comments

  1. Great summary! The only item missing is the baseline when not to use automatic type deduction at all. Many-a-times a container/range under question has completely non-generic, commonly used, simple and terse types (unlike the elements of std::map as in your example), in which case usage of auto in any form is not only unnecessary, but obfuscating.

    I fear that reading this wonderfully written article, a colleague would mis-quote “Use auto& when you want to modify elements in the range in non-generic code” to mean, replace all occurrences of the following perfectly readable code:

     someContainer<int> values;
     // ... some where down far along
     for( int& i : values ) { ... }
    

    with

    someContainer<int> values;
    // ...
    for( auto& i : values ) { /* what type is i again? */ }
    
    Reply
    • Thank you for bringing this up. The article focuses purely on the question “When using auto type deduction with range-based for loops, which variant should I use?”. The “When should I use auto type deduction?” question is for a whole other debate :).

      Reply
  2. I don’t find the argument for auto& over auto&& in non-generic code compelling.

    I think this is better advice and simpler to read:

    copy: auto
    reference: const auto &
    mutable reference: auto&&

    Now you don’t have to worry about caveats or errors with auto& vs auto&& and the rule is as simple as adding a second ampersand. Besides, 3 rules that each describe use intent is better than 4 rules that mix use case and code locality.

    Reply
    • Yes, this is an alternative advice. And maybe in the future, when there is greater awareness of forwarding references among C++ programmers, I will encourage that. However, for the time being, I still think my advice will lead to less-confusing code. That’s just my opinion, though.

      Reply
      • Yeah, that’s fair. While we disagree, it’s more a matter of taste. I like to imagine a future where universal references (is that what they are being called still?) are commonplace and don’t trip people up.

        The article is pretty good in all, though, so thanks for writing it. :)

        Reply

Leave a Comment.