Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any reason to not use auto& for C++ range-based for-loops?

Tags:

c++

For example, the loop:

std::vector<int> vec;
...
for (auto& c : vec) { ... }

will iterate over vec and copy each element by reference.

Would there ever be a reason to do this?

for (int& c : vec) { ... }
like image 873
user473973 Avatar asked Dec 08 '22 10:12

user473973


2 Answers

The two code snippets will result in the same code being generated: with auto, the compiler will figure out that the underlying type is int, and do exactly the same thing.

However, the option with auto is more "future-proof": if at some later date you decide that int should be replaced with, say, uint8_t to save space, you wouldn't need to go through your code looking for references to the underlying type that may need to be changed, because the compiler will do it for you automatically.

like image 132
Sergey Kalinichenko Avatar answered Dec 19 '22 17:12

Sergey Kalinichenko


Use auto wherever it makes the code better, but nowhere else. Understand the impact using auto overly-liberally has on maintainability.

The question here really is if there is any reason why you shouldn't use auto for anything you can.

Well, let's get one thing out of the way first of all. The fundamental reason why auto was introduced in the first place was two-fold.

First, it makes declarations of complex-type variables simpler and easier to read and understand. This is especially true when declaring an iterator in a for loop. Consider the C++03 psudocode:

for (std::vector <Foo>::const_iterator it = myFoos.begin(); it != myFoos.end(); ++it)

This can become much more complex as the complexity of myFoos's type becomes more complex. Moreover if the type of myFoos is changed in a subtle way, but in a way that's insignifigant to the loop just written, the complex declaration must be revisited. This is a maintennance problem made simpler in C++11:

for (auto it = myFoos.begin(); it != myFoos.end(); ++it)

Second, there are situations which arise that are impossible to deal with without the facilities provided by auto and it's sibling, decltype. This comes up in templates. Consider (source):

template<typename T, typename S>
void foo(T lhs, S rhs) {
  auto prod = lhs * rhs;
  //...
}

In C++03 the type of prod cannot always be inferred if the types of lhs and rhs are not the same. In C++11 this is possible using auto. Alas it is also possible using decltype, but this was also added to C++11 along with auto.


Many of the C++ elite suggest that you should use auto anywhere possible. The reason for this was stated by Herb Sutter in a recent conference:

It's shorter. It's more convinient. It's more future-proof, because if you change your function's return type, auto just works.

However they also acknowledge the "sharp edges." There are many cases where auto doesn't do what you want or expect. These sharp edges can cut you when you want a type conversion.


So on the one hand we have a highly respected camp telling us "use auto everywhere possible, but nowhere else." This doesn't feel right to me however. Most of the benefits that auto provide are provided at what I'm going to call "first-write time." The time when you are first writing a piece of code. As you're writing a big chink of brand-new code you can go ahead and use auto virtually everywhere and get exactly the behavior you expect. As you're writing the code you know exactly what's going on with your types and variables. I don't know about you, but as I write code there is a constant stream of thoughts going through my head. How do I want to create this loop? What kind of thing do I want that function to return so I can use it here? How can I write this so that is fast, correct, and easy to understand 6 months from now? Most of this is never expressed directly in the code that I write, except that the code that I write is a direct result of these thoughts.

At that time, using auto would make writing this code simpler and easier. I don't have to burden my mind with all the little minute of signed versus unsigned, reference vs value, 32 bit vs 64 bit, etc. I just write auto and everything works.

But here's my problem with auto. 6 months later when revisiting this code to add some major new functionality, that buzz of thought that was going through my mind as I first write the code has long been extinguished. My buffers were flushed long ago, and none of those thought are with me anymore. If I have done my job well, then the essence of those thoughts are expressed directly in the code I wrote. I can reverse-engineer what I was thinking by just looking at the structure of my functions and data types.

If auto is sprinkled everywhere, a big part of that cognizance is lost. I don't know what I was thinking would happen with this datatype because now it's inferred. If there was a subtle relationship taking place with an operator, that relationship is no longer expressed by the datatypes -- it's all just auto.

Maintenance becomes more difficult because no I have to re-create much of that thought. Subtle relationships become more clouded, and everything is just harder to understand.

So I'm not a fan of using auto everywhere possible. I think that makes maintenance harder than it has to be. That's not to say I believe that auto should only be used where it's required. Rather, it's a balancing act. The four criteria I use to judge the quality of my (or anyone's code) are: efficiency, correctness, robustness, and maintainability. In no particular order. If we design a spectrum of auto use where one side is "purely optional" and the other side is "strictly required", I feel that in general the closer to "purely optional" we get, the more maintainability suffers.

All this to say, finally, that my philosophy can be nutshelled as:

Use auto wherever it makes the code better, but nowhere else. Understand the impact using auto overly-liberally has on maintainability.

like image 36
John Dibling Avatar answered Dec 19 '22 16:12

John Dibling