16

I'm reading someone else's code and they separately increment their for loop counter inside the loop, as well as including the usual afterthought. For example:

for( int y = 4; y < 12; y++ ) {
    // blah
    if( var < othervar ) {
        y++;
    }
    // blah
}

Based on the majority of code others have written and read, should I be expecting to see this?

GhostCat
  • 137,827
  • 25
  • 176
  • 248
volvox
  • 3,014
  • 16
  • 51
  • 80
  • 23
    IMO this is extremely situation dependent. – AndyG Dec 29 '16 at 13:59
  • Thanks; could you please briefly alude to when it is OK? – volvox Dec 29 '16 at 14:00
  • 3
    Obviously not: a) it makes code harder to read, understand. b) it may lead to eternal loop in general c) all future fixes in those part of code have to be done keeping in mind they don't make it eternal. – Arkady Dec 29 '16 at 14:00
  • yeah, that's unusual. – Giorgi Moniava Dec 29 '16 at 14:04
  • 3
    @Arkady How would it lead to an eternal loop? (Note: the condition is `y < 12`, not `y != 12`. If it had used `!=`, I would've understood your comment.) –  Dec 29 '16 at 14:15
  • @hvd not in that example, in general such practice can lead to eternal loop :-) Because when you look at `for` line, you don't know how many iterations will be done. 0, 1, 10000, or eternal, because variable is somehow changing inside. – Arkady Dec 29 '16 at 14:18
  • 2
    What is the question? Is it about "good practice" or is it about "what you should expect to see"? That is two very different questions. – Support Ukraine Dec 29 '16 at 15:55
  • That's because I had to edit what I was asking in order for the q to be unheld. I will edit the question title. – volvox Dec 29 '16 at 16:38
  • 2
    To me, this is a code smell, *always*. Even if it's functionally brilliant, the readability is atrocious. – Bradley Thomas Dec 29 '16 at 16:42
  • 4
    Sorry to be the naysayer, but I still fail so see how this is not opinion-based. Maybe this is a better fit a [programmers.se]? – Sourav Ghosh Dec 29 '16 at 17:02
  • FWIW, the usual C idiom would be to use a leading `if` resulting in a `continue` instead of direct loop counter increment, since it's much more clear what the intent is, i.e. `for( int i = 0; i < MAX; i++ ) { if ( condition() ) { continue; } process1(); process2(); }` instead of `for( int i = 0; i < MAX; i++ ) { process1(); process2(); if ( condition() ) { i++; } }` (note that these loops are quite different in actual execution!) –  Dec 29 '16 at 17:32
  • 2
    If your intention is to skip current iteration, `continue` does exactly that – GingerPlusPlus Dec 29 '16 at 18:20

7 Answers7

32

The practice of manipulating the loop counter within a for loop is not exactly widespread. It would surprise many of the people reading that code. And surprising your readers is rarely a good idea.

The additional manipulation of your loop counter adds a ton of complexity to your code because you have to keep in mind what it means and how it affects the overall behavior of the loop. As Arkady mentioned, it makes your code much harder to maintain.

To put it simply, avoid this pattern. When you follow "clean code" principles, especially the single layer of abstraction (SLA) principle, there is no such thing as

for(something)
  if (somethingElse)
   y++

Following the principle requires you to move that if block into its own method, making it awkward to manipulate some outer counter within that method.

But beyond that, there might be situations where "something" like your example makes; but for those cases - why not use a while loop then?

In other words: the thing that makes your example complicated and confusing is the fact that two different parts of the code change your loop counter. So another approach could look like:

 while (y < whatever) {
   ...
   y = determineY(y, who, knows);
 }

That new method could then be the central place to figure how to update the loop variable.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • 1
    Indeed, these are facts not opinions. – A.S.H Dec 29 '16 at 14:37
  • 7
    Except that the practice is widespread. – Oktalist Dec 29 '16 at 15:45
  • 5
    `And surprising your readers is never a good idea.` is an opinion. A very common one, but just an opinion. As is the "fact" that following clean code principles is always a good idea. – Mad Physicist Dec 29 '16 at 16:26
  • 4
    Well, sometimes you *do* want to skip an iteration if a condition is met. Adding an `y++` inside the loop may achieve this. Sure it depends on the context and probably requires an explicit comment about what's going on, but I don't see many better alternatives that wouldn't require an explicit comment anyway... – Bakuriu Dec 29 '16 at 16:47
  • 5
    @mad Can you give an example of where it would be desirable to write code that surprised its readers? Otherwise, your comment just seems like excessive pedantry, claiming that commonly-held opinions and highly-useful guidelines are "just opinions". Virtually everything is an opinion; this is GhostCat's, it has his name attached to it. – Cody Gray - on strike Dec 29 '16 at 18:18
  • 2
    @CodyGray, the question quickly becomes philosophical. Who are your readers and what it takes to surprise them? For example, it's quite easy to surprise Google developers based on their guidelines (at least one's I've seen some time ago), which claim that templates in C++ are controversial, and if you find yourself using template template parameter, you are doing something wrong. I do not like the idea of pulling down to the lowest common denominator in software development. – SergeyA Dec 29 '16 at 20:36
  • @CodyGray. I happen to agree with GhostCat's points for the most part, at least in the abstract sense. As SergyA points out, the question is of degree. Code that you wrote thinking it is unsurprising and continue to think so months later when you revisit it may take me hours to figure out. To answer your exact question through, the only place I can think of off the top of my head where surprises are valid would be a non-premature optimization step. – Mad Physicist Dec 29 '16 at 21:29
  • 1
    Well, everyone loves to bring up Google's C++ style guide because it is so utterly ridiculous. Making the question overly philosophical is not practical, as far as I'm concerned. You assume that the reader is a competent C++ programmer, familiar with all aspects of the language or able to look up that with which she is unfamiliar. Avoiding templates or exceptions out of fear that the next programmer doesn't know about them is pointless, but it is immediately obvious if you *don't* understand them. The same can't be said about incrementing a variable in a `for` loop, too easy to escape notice. – Cody Gray - on strike Dec 30 '16 at 06:18
  • In the case of optimization doing something non-obvious, whether premature or otherwise, a comment would be required. That would probably solve the concerns here, as well, if you didn't feel that rewriting the whole thing as a `while` loop was clearer (which I do). – Cody Gray - on strike Dec 30 '16 at 06:19
  • @GhostCat by now, you should know better than me that getting 0 or -1 on questions or answers don't mean necessarily that they are bad, it can be due to the fact that you simply did not "follow the SO's rules" or simply because it is so specific that the audience is very limited so there is not much people able to vote. I quickly checked the 5 first questions with 0 vote, it seems that you are in the second category (too specific => limited audience). Personally my best answers are not the most voted ones for the same reasons. – Nicolas Filotto Jul 31 '18 at 07:00
  • @NicolasFilotto Thanks, that was quick ;-) ... seems you are spending more time here again! – GhostCat Jul 31 '18 at 07:07
27

I beg to differ with the acclaimed answer above. There is nothing wrong with manipulating loop control variable inside the loop body. For example, here is the classical example of cleaning up the map:

for (auto it = map.begin(), e = map.end(); it != e; ) {
    if (it->second == 10)
        it = map.erase(it);
    else
        ++it;
}

Since I have been rightfully pointed out to the fact that iterators are not the same as numeric control variable, let's consider an example of parsing the string. Let's assume the string consists of a series of characters, where characters prefixed with '\' are considered to be special and need to be skipped:

for (size_t i = 0; i < s_len; ++i) {
    if (s[i] == '\\') {
       ++i;
       continue;
    }
    process_symbol(s[i]);
}
SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • 1
    But that's a special case dealing with the particular nature of removing items from maps. It doesn't follow the more general case of performing some action a number of times. You certainly wouldn't use that form when simply traversing the map. – Rob K Dec 29 '16 at 15:20
  • 1
    I beg to differ that the original example is about manipulating a *numerical* loop counter in two places... Which is in my eyes not at all the same as your code that works on an iterator. Especially as your example is changing the value of it only in place, not two! – GhostCat Dec 29 '16 at 16:27
  • 1
    @GhostCat, I just used a very clear example. The other one would be a numeric counter which to be used when parsing string with screened symbols. Let me give an example in the answer. – SergeyA Dec 29 '16 at 16:49
  • 2
    @RobK Well, I believe in the question it is clear that the intention of the loop is **not** to execute an action "k times", since otherwise why would you start with `y=4`? To screw with the reader? The purpose of the loop is to iterate over a sequence of (mostly) consecutive numbers and do something for each of those values. Sometimes you have to skip a value (and adding an `y++` would do that perfectly). – Bakuriu Dec 29 '16 at 16:50
  • The difference is that in your `for` loop, the third statement where the loop variable is modified, is left empty. People will naturally assume that the modification happen inside the loop. – Siyuan Ren Dec 29 '16 at 16:52
  • 1
    @SiyuanRen, see updated answer, I have included just that. – SergeyA Dec 29 '16 at 16:58
  • 1
    Wouldn't it be better to let `process_symbol()` just ignore the special character(s) so that logic is where it belongs and doesn't clutter up the `for` loop? – Chimera Dec 29 '16 at 18:26
  • 2
    @Chimera That's not possible. The idea is that even if e.g. `$` is a special symbol, `\$` is not special. `process_symbol` only gets given one character at a time, so cannot see that the `$` was preceded by ```\```. –  Dec 29 '16 at 18:47
  • That said, there is a potential bug here. If ```\``` is supposed to escape the next character, then putting a sole backslash at the end of a string should be an error. The increment inside the loop causes the backslash to be silently ignored, and I can sympathise with a point of view that says this is hard to understand, and should have been written differently. (I wouldn't agree, I think the basic concept of `++i; continue;` is fine here, so long as error checking is added, but I can understand why others may dislike it.) –  Dec 29 '16 at 18:50
  • @hvd Makes sense. I was for some reason thinking that the special characters weren't preceded by '\'. I didn't read the answer closely enough. – Chimera Dec 29 '16 at 18:52
  • Forgive me for playing devil's advocate, but even in the `std::map` example above, why not transform it into an (arguably more readable) `auto it = map.begin(); while(it != map.end()) {...}` since that's basically what a `for` loop with an empty third statement is? – LS97 Dec 29 '16 at 19:13
  • @hvd, yes, I even made a note to that in my answer but removed it before posting. The idea is that this is somehow checked before or simply part of the contract. – SergeyA Dec 29 '16 at 19:21
  • 2
    @LS97, visibility of `it` would be one reason. – SergeyA Dec 29 '16 at 19:22
10

Use a while loop instead.

While you can do this with a for loop, you should not. Remember that a program is like any other piece of communication, and must be done with your audience in mind. For a program, the audience includes the compiler and the next person to do maintenance on the code (likely you in about 6 months).

To the compiler, the code is taken very literally -- set up a index variable, run the loop body, execute the increment, then check the condition to see if you are looping again. The compiler doesn't care if you monkey with the loop index.

To a person however, a for loop has a specific implied meaning: Run this loop a fixed number of times. If you monkey with the loop index, then this violates the implication. It's dishonest in a sense, and it matters because the next person to read the code will either have to spend extra effort to understand the loop, or will fail to do so and will therefore fail to understand.

If you want to monkey with the loop index, use a while loop. Especially in C/C++/related languages, a for loop is exactly as powerful as a while loop, so you never lose any power or expressiveness. Any for loop can be converted to a while loop and vice versa. However, the next person who reads it won't depend on the implication that you don't monkey with the loop index. Making it a while loop instead of a for loop is a warning that this kind of loop may be more complicated, and in your case, it is in fact more complicated.

kwan3217
  • 239
  • 1
  • 5
7

If you increment inside the loop, make sure to comment it. A canonical example (based on a Scott Meyers Effective C++ item) is given in the Q&A How to remove from a map while iterating it? (verbatim code copy)

for (auto it = m.cbegin(); it != m.cend() /* not hoisted */; /* no increment */)
{
  if (must_delete)
  {
    m.erase(it++);    // or "it = m.erase(it)" since C++11
  }
  else
  {
    ++it;
  }
}

Here, both the non-constant nature of the end() iterator and the increment inside the loop are surprising, so they need to be documented. Note: the loop hoisting here is after all possible so probably should be done for code clarity.

Community
  • 1
  • 1
TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • 2
    Bright minds think alike :) But `end` iterator is not invalidated, so you need not to call for it on every iteration. – SergeyA Dec 29 '16 at 15:15
3

For what it's worth, here is what the C++ Core Guidelines has to say on the subject:

http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-loop-counter

ES.86: Avoid modifying loop control variables inside the body of raw for-loops

Reason The loop control up front should enable correct reasoning about what is happening inside the loop. Modifying loop counters in both the iteration-expression and inside the body of the loop is a perennial source of surprises and bugs.

Also note that in the other answers here that discuss the case with std::map, the increment of the control variable is still only done once per iteration, where in your example, it can be done more than once per iteration.

Community
  • 1
  • 1
Mikel F
  • 3,567
  • 1
  • 21
  • 33
3

So after the some confusion, i.e. close, reopen, question body update, title update, I think the question is finally clear. And also no longer opinion based.

As I understand it the question is:

When I look at code written by others, should I be expecting to see "loop condition variable" being changed in the loop body ?

The answer to this is a clear:

yes

When you work with others code - regardless of whether you do a review, fix a bug, add a new feature - you shall expect the worst.

Everything that are valid within the language is to be expected.

Don't make any assumptions about the code being in acordance with any good practice.

Community
  • 1
  • 1
Support Ukraine
  • 42,271
  • 4
  • 38
  • 63
2

It's really better to write as a while loop

y = 4;
while(y < 12)
{
   /* body */
   if(condition)
     y++;
   y++;
}

You can sometimes separate out the loop logic from the body

 while(y < 12)
 {
    /* body */
    y += condition ? 2 : 1;
 }

I would allow the for() method if and only if you rarely "skip" an item, like escapes in a quoted string.

Malcolm McLean
  • 6,258
  • 1
  • 17
  • 18