'break' and 'continue' are "effectively restricted forms of
the goto statement."[1] As such, they suffer some of the
same criticism levied against the goto statement 40 years
ago[2].
You might also add exceptions and returning from the middle of a
function, or calling exit(), abort() or some other function
which never returns.
I went back and reread [2] not too long ago. Dijkstra makes
two main arguments against goto, neither of which applies with
nearly the same force to break or continue.
His first argument is that every goto requires a label, and it
is difficult to determine by inspection what claims you can
make about the state of a program when execution passes a
label. The reason, of course, is that you have to inspect the
entire program to find every goto that refers to that label,
and then verify that the state of the program at that goto is
what you desire. This argument does not apply to break or
continue, because neither of them uses a label. Indeed, all
you have to do is to ensure that at each break or continue,
the conditions that you expected the surrounding block to
satisfy have been satifsied.
There are, in fact, two separate issues involved here. One is
program structure, and the other is how you implement it. In
Fortran IV, you had to use goto, for example. This didn't mean
that you couldn't write well structured code. It did mean that
unless the reader was 100% sure that you had done so, there was
nothing in the language which would help in determining it.
(One of the best structured programs I ever saw was written in
Fortran IV.)
In C++, of course, there's no reason to use goto except for
unstructured control flow. (It's interesting to note that
Pascal had a goto. Most of the uses I've seen of it, however,
would be better handled by an exception in C++.)
His second argument is that it is hard to talk about the
execution state (i.e. how much progress the program has made,
irrespective of the values of its variables) of a program that
uses goto statements. If there are just loops and function
calls, then all you need to keep track of this state is the
call trace and how many times each currently active loop has
executed. But if you introduce goto statements, this compact
technique becomes impossible. Instead, you have to remember
everything that has happened. Again, these objections do not
apply to break or continue because they are not nearly so
disruptive of the flow of control.
And that argument hinges on the structure. The problem isn't
the goto per se, but what you do with it.
Break and continue do have problems here. As you say, nowhere
near as many as arbitrary goto's, but they do disrupt program
flow, and make reasoning about correctness more difficult.
In the example which started this thread, Alf very carefully (I
presume) chose an example of a loop and a half, where there was
only a single break, and that break was the only way to leave
the loop. Code that is, in fact, single entry/single exit.
Although I feel that even this code is better written with the
test up front (since that establishes a single loop invariant,
rather than having a weak invariant for part of the loop, and a
stronger one for the second part), such use of break is
certainly less disruptive than just arbitrary use, say with a
condition at the top as well.
So although I would agree with you that break and continue
suffer some of the criticisms of goto, I also think they are
nowhere near in the same league because of the limited ways in
which they can transfer control.
I think it's a question of use. Start using if's nested three
layers deep, and throw in a break in one of them, and a continue
in another, and it can be pretty disruptive as well. Used as
the single exit of a loop and a half, the only problem is the
fact that you have to deal with two different invariants in the
loop, rather than one. Still not the best solution, but not
nearly as bad as a lot of other things.