O "problema" com os comandos break e continue estão mais relacionados a um erro de interpretação do célebre artigo Goto considered harmful de Edgar Dijkstra, que propriamente a um problema destes comandos em si.
Quando se escreve programas em assembly, é inevitável escrever estruturas condicionais com jumps. Considere o trecho de código abaixo escrito em assembly MIPS:
# Seleciona o quadrante ao qual o ângulo pertence
beq $t1,$zero,quad1
addi $t1,$t1,-1
beq $t1,$zero,quad2
addi $t1,$t1,-1
beq $t1,$zero,quad3
quad4: li.s $f0,1.0
s.s $f0,cSig
li.s $f0,-1.0
s.s $f0,sSig
li $t1,360
sub $t0,$t1,$t0
j toRad
quad3: li.s $f0,-1.0
s.s $f0,cSig
li.s $f0,-1.0
s.s $f0,sSig
addi $t0,$t0,-180
j toRad
quad2: li.s $f0,-1.0
s.s $f0,cSig
li.s $f0,1.0
s.s $f0,sSig
li $t1,180
sub $t0,$t1,$t0
j toRad
quad1: li.s $f0,1.0
s.s $f0,cSig
li.s $f0,1.0
s.s $f0,sSig
# Transforma o valor do ângulo de graus para radianos
toRad: mtc1 $t0,$f0
cvt.d.w $f0,$f0
s.d $f0,angle
l.d $f2,numPi
mul.d $f0,$f0,$f2
li.d $f2,180.0
s.d $f0,angle
De forma bastante resumida, esse código pode ser entendido em alto nível como:
switch (valor) {
case quad4: ... ; break;
case quad3: ... ; break;
case quad2: ... ; break;
case quad1: ... ;
}
// endereço da primeira instrução após o bloco switch
toRad: ...
Note que o mesmo código poderia ser escrito como:
switch (valor) {
case quad4: ... ; goto toRad;
case quad3: ... ; goto toRad;
case quad2: ... ; goto toRad;
case quad1: ... ;
}
// endereço da primeira instrução após o bloco switch
toRad: ...
ou pior ainda
if (valor == quad4) {
...
goto toRad;
}
if (valor == quad3) {
...
goto toRad;
}
if (valor == quad2) {
...
goto toRad;
}
if (valor == quad1) {
...
}
// endereço da primeira instrução após o bloco switch
toRad: ...
Se você pegar código Fortran, por exemplo, verá que mesmo em uma linguagem de alto nível era prática comum fazer uso de labels na escrita do código. Era contra este tipo de prática que Dijkstra advoga, devido à tendência de criar código espaguete, o qual é difícil de ler e, consequentemente, manter.
Em sua defesa da programação estruturada, Dijkstra defendia que nossa capacidade de acompanhar processos dinâmicos complexos é, em geral, limitada. Por este motivo, devíamos adotar métodos mais "literários" de programação, que tornem a estrutura de fluxo e o objetivo do código mais transparentes, escondendo aspectos de baixo nível.
No caso específico dos breaks, note que são localizados dentro de suas estruturas de controle e possuem semântica muito clara, quebrar a execução, o que não fere em nada a legibilidade do programa, pelo contrário. É bem verdade que em algumas situações seu uso poderia ser evitado de forma mais elegante pelo uso de uma flag, por exemplo, mas como tudo na programação, não há regra de ouro, depende do caso. Mesmo um goto, bem utilizado, tem seu espaço. Veja o exemplo abaixo:
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (condicaoTerminalPrematura) goto termineAmbos;
...
}
...
}
termineAmbos:
Neste exemplo, o goto funciona para quebrar a execução de ambos os laços simultaneamente, sendo que o endereço de salto é localmente próximo, e o nome bastante sugestivo. Tente achar uma solução mais auto-descritiva, e, ouso dizer, elegante, para esse problema.
Em realidade, programação estruturada é muito mais do que usar ou não breaks. Trata-se de escrever código modularizado, com funções pequenas e reutilizáveis; a máxima KISS (Keep it simple, stupid), funções e programas devem fazer uma única coisa, e fazê-la bem feita. Dispor o código de forma mais sequencial possível, de forma que não seja preciso subir e descer para entender o que o programa faz. Evitar fazer uso de artifícios que dificultem o entendimento, por exemplo abusar de side-effects obscuros. Tudo deve ser o mais transparente possível. O objetivo central é descrever o processo de forma lógica e legível.