Mais uma vez: Nem sempre é bom desenvolver diretamente em assembly!!!

Sim… de fato, a máxima “Não existe melhor otimizador do que o está entre suas orelhas” continua sendo verdadeira, só que essa massa de carne nem sempre realiza o melhor trabalho. Eis um caso onde uma rotina simples deveria ser mais rápida se feita em ASM:

uint16_t cksum(void *data, size_t length)
{
  uint32_t sum;
  uint16_t *p = data;
  _Bool rem;

  sum = 0;
  rem = length & 1;
  length >>= 1;

  while (length--)
    sum += *p++;

  if (rem)
    sum += *(uint8_t *)p;

  while (sum >> 16)
    sum = (sum & 0xffff) + (sum >> 16);

  return ~sum;
}

Essa é a implementação do cálculo de CHECKSUM de 16 bits, de acordo com a RFC 1071 (aqui). O código é bem simples e deveria gerar uma listagem em assembly tão simples quanto, mas quando você compila com a máxima otimização consegue ums listagem enorme que, inclusive, na plataforma x86-64, usa até SSE!

A minha implementação da rotina acima, em puro assembly e “otimizada”, para a plataforma x86-64, seria essa:

  bits 64

  section .text

  global cksum2

; uint16_t cksum2(void *data, size_t length);
  align 16
cksum2:
  xor   eax,eax   ; sum = 0;
  xor   ecx,ecx   ; RCX será usado como índice...
  shr   rsi,1     ; # de words!
  jnc   .even     ; Se CF=0, RSI era par!

  ; Ao invés de adicionar o byte extra no final,
  ; optei pelo início.
  movzx dx,byte [rdi]
  add   ax,dx
  inc   rdi
  jmp   .even

  ; Loops geralmente são críticos. Note que optei
  ; por colocar o teste no final, deixando o salto
  ; conticional para trás para não ferir o
  ; algoritmo estático do "branch prediction".
  ; Também, o ponto de entrada alinhado aos 16 bytes
  ; ajuda com possíveis efeitos negativos com o 
  ; cache L1i...
  align 16
.loop:
  add   ax,[rdi+rcx*2]
  adc   ax,0
  dec   rsi        ; length--;
  inc   rcx        ; próxima word...
.even:
  test  rsi,rsi    ; length == 0?
  jnz   .loop      ; não! continua no loop.
  
  not   ax         ; sum = ~sum;
  ret

Compare essa minha rotina com a gerada pelo compilador:

cksum:
  mov   r11,rsi
  shr   rsi
  push  rbp
  and   r11d,1
  test  rsi,rsi
  push  rbx
  je    .L2
  mov   rdx,rdi
  lea   r10,[rsi-1]
  and   edx,15
  shr   rdx
  neg   rdx
  and   edx,7
  cmp   rdx,rsi
  cmova rdx,rsi
  cmp   rsi,10
  ja    .L74
  mov   rdx,rsi
.L3:
  cmp   rdx,1
  lea   rcx,[rdi+2]
  movzx eax,WORD PTR [rdi]
  lea   r8,[rsi-2]
  je    .L5
  movzx r8d,WORD PTR [rdi+2]
  lea   rcx,[rdi+4]
  add   eax,r8d
  cmp   rdx,2
  lea   r8,[rsi-3]
  je    .L5
  movzx r8d,WORD PTR [rdi+4]
  lea   rcx,[rdi+6]
  add   eax,r8d
  cmp   rdx,3
  lea   r8,[rsi-4]
  je    .L5
  movzx r8d,WORD PTR [rdi+6]
  lea   rcx,[rdi+8]
  add   eax,r8d
  cmp   rdx,4
  lea   r8,[rsi-5]
  je    .L5
  movzx r8d,WORD PTR [rdi+8]
  lea   rcx,[rdi+10]
  add   eax,r8d
  cmp   rdx,5
  lea   r8,[rsi-6]
  je    .L5
  movzx r8d,WORD PTR [rdi+10]
  lea   rcx,[rdi+12]
  add   eax,r8d
  cmp   rdx,6
  lea   r8,[rsi-7]
  je    .L5
  movzx r8d,WORD PTR [rdi+12]
  lea   rcx,[rdi+14]
  add   eax,r8d
  cmp   rdx,7
  lea   r8,[rsi-8]
  je    .L5
  movzx r8d,WORD PTR [rdi+14]
  lea   rcx,[rdi+16]
  add   eax,r8d
  cmp   rdx,8
  lea   r8,[rsi-9]
  je    .L5
  movzx r8d,WORD PTR [rdi+16]
  lea   rcx,[rdi+18]
  add   eax,r8d
  cmp   rdx,10
  lea   r8,[rsi-10]
  jne   .L5
  movzx r8d,WORD PTR [rdi+18]
  lea   rcx,[rdi+20]
  add   eax,r8d
  lea   r8,[rsi-11]
.L5:
  cmp   rsi,rdx
  je    .L6
.L4:
  mov   rbx,rsi
  sub   r10,rdx
  sub   rbx,rdx
  lea   r9,[rbx-8]
  shr   r9,3
  add   r9,1
  cmp   r10,6
  lea   rbp,[0+r9*8]
  jbe   .L7
  pxor  xmm0,xmm0
  lea   r10,[rdi+rdx*2]
  xor   edx,edx
  pxor  xmm2,xmm2
.L8:
  movdqa  xmm1,XMMWORD PTR [r10]
  add   rdx,1
  add   r10,16
  cmp   r9,rdx
  movdqa  xmm3,xmm1
  punpckhwd xmm1,xmm2
  punpcklwd xmm3,xmm2
  paddd xmm0,xmm3
  paddd xmm0,xmm1
  ja    .L8
  movdqa  xmm1,xmm0
  sub   r8,rbp
  lea   rcx,[rcx+rbp*2]
  psrldq  xmm1,8
  paddd xmm0,xmm1
  movdqa  xmm1,xmm0
  psrldq  xmm1,4
  paddd xmm0,xmm1
  movd  edx,xmm0
  add   eax,edx
  cmp   rbx,rbp
  je    .L6
.L7:
  movzx edx,WORD PTR [rcx]
  add   eax,edx
  test  r8,r8
  je    .L6
  movzx edx,WORD PTR [rcx+2]
  add   eax,edx
  cmp   r8,1
  je    .L6
  movzx edx,WORD PTR [rcx+4]
  add   eax,edx
  cmp   r8,2
  je    .L6
  movzx edx,WORD PTR [rcx+6]
  add   eax,edx
  cmp   r8,3
  je    .L6
  movzx edx,WORD PTR [rcx+8]
  add   eax,edx
  cmp   r8,4
  je    .L6
  movzx edx,WORD PTR [rcx+10]
  add   eax,edx
  cmp   r8,5
  je    .L6
  movzx edx,WORD PTR [rcx+12]
  add   eax,edx
.L6:
  test  r11,r11
  lea   rdi,[rdi+rsi*2]
  je    .L71
.L17:
  movzx edx,BYTE PTR [rdi]
  add   eax,edx
  mov   edx,eax
  shr   edx,16
  test  edx,edx
  je    .L75
.L49:
  movzx eax,ax
  add   eax,edx
.L71:
  mov   edx,eax
  shr   edx,16
  test  edx,edx
  jne   .L49
.L75:
  not   eax
.L67:
  pop   rbx
  pop   rbp
  ret
.L74:
  test  rdx,rdx
  jne   .L3
  mov   r8,r10
  mov   rcx,rdi
  xor   eax,eax
  jmp   .L4
.L2:
  test  r11,r11
  je    .L76
  xor   eax,eax
  jmp   .L17
.L76:
  mov   eax,-1
  jmp   .L67

UAU! Obviamente a minha rotina é mais rápida que esse caminhão de instruções, certo? (aliás, repare nas instruções entre os labels .L8 e .L7! hehe)

Infelizmente os testes mostram que não! Comparando o cálculo do checksum de um buffer de 2 KiB (menos 1 byte para termos um buffer de tamanho impar, ou seja, o pior caso), a minha rotina é 5 vezes mais lenta que a gerada pelo compilador! Num de minhas máquinas de teste obtive, para cksum(), 3432 ciclos e para cksum2(), 16872!

É interessante notar que se mudarmos o código em C para efetuar a adição do byte isolado, se houver um, a performance vai ser semelhante à obtida em cksum2():

uint16_t cksum3(void *data, size_t length)
{
  uint32_t sum;
  uint16_t *p = data;

  sum = 0;
  if (length & 1)
    sum += *(uint8_t *)p++;
  length >>= 1;

  while (length--)
    sum += *p++;

  while (sum >> 16)
    sum = (sum & 0xffff) + (sum >> 16);

  return ~sum;
}

E, também, se mudar o meu código para efetuar a soma do byte adicional no final, se houver um, como na rotina original, a performance continuará tão ruim quanto:

  align 16
cksum4:
  xor   eax,eax
  xor   ecx,ecx
  mov   edx,esi
  and   edx,1
  shr   rsi,1
  jmp   .even

  align 16
.loop:
  add   ax,[rdi+rcx*2]
  adc   ax,0
  dec   rsi
  inc   rcx
.even:
  jnz   .loop
  test  edx,edx
  jz    .exit
  add   ax,[rdi+rcx*2]
  adc   ax,0
.exit:
  not   ax
  ret

Ou seja, o compilador está tomando conta dos possíveis efeitos no cache e desenrolando os loops para que atinja a melhor performance possível, mesmo que, para isso, gere um código bem maluco. Coisa que apenas com muita experiência eu ou você poderíamos fazer…

Novamente, sempre meça a performance de seus códigos e não pense que só porque está em “assembly” é que você conseguirá a melhor performance!

Arredondamento. Qual é o “correto”?

O último artigo gerou alguma controvérsia… Alguns leitores perguntam: “Qual é o jeito certo?” e não se satisfazem com a resposta de que ambos os métodos, de truncamento e de arredondamento para baixo, estão certos, dependendo da aplicação. Aqui quero provocá-los mostrando algo similar…

Já mostrei antes que operações em ponto flutuante dependem de algum método de arredondamento bem definido. O padrão IEEE 754 define quatro deles:

  • No sentido de +\infty;
  • No sentido de -\infty;
  • No sentido de 0;
  • O valor “mais próximo”;

Ao calcular f=1.0/10.0, o valor 0.1, que não pode ser representado numa variável do tipo ponto flutuante, já que, em binário, será uma dízima periódica:

\displaystyle (0.1)_{10} = (0.0110011001100110011001100...)_{2}

Portanto, precisa ser arredondado. Se estivermos lidando com o tipo float esse valor será (1.1001100110011001100110?)_{2}\cdot 2^{-2}, onde ‘?’ é o algarismo impreciso… Ou seja, se ele for zero, termos um valor levemente inferior a 0.1, se ele for 1, teremos um valor levemente superior a 0.1. Qual deles devemos escolher?

Por default, o padrão IEEE 754 escolhe o método do “mais próximo”. Repare:

/* test.c */
#include <stdio.h>

extern float _mydiv(float, float);

void main(void)
{
  float f;
  unsigned int d = 0x3dcccccc;
  float *g = (float *)&d;
  unsigned int *p = (unsigned int *)&f;

  f = _mydiv(1.0f, 10.0f);

  printf("O valor '%.2f' é codificado como '0x%08X'.\n\n", 
    f, *p);

  printf("Mas, na verdade:\n"
         "0x%08X -> %.18f\n"
         "0x%08X -> %.18f\n",
    *p, f,
    d, *g);
}

O motivo de criar a função _mydiv é para evitar que o compilador otimize a operação de divisão entre duas constantes e não faça divisão alguma. Neste caso, o arredondamento será feito pelo compilador, não pelo nosso programa. A função _mydiv é, simplesmente:

/* mydiv.c */
float _mydiv(float a, float b) 
{ return a/b; }

Compilando e linkando tudo e depois executando, temos:

$ gcc -o test test.c mydiv.c
$ ./test
O valor '0.10' é codificado como '0x3DCCCCCD'.

Mas, na verdade:
0x3DCCCCCD -> 0.100000001490116119
0x3DCCCCCC -> 0.099999994039535522

É fácil perceber que o primeiro caso está mais próximo de 0.1 do que o segundo e é este que o processador escolherá (como isso é feito não interessa agora!). Mas, isso não significa que os outros métodos de arredondamento não sejam “corretos”. De fato, se mandarmos o processador arredondar de outra maneira ele o fará, Eis um exemplo:

/* test.c */
#include <stdio.h>
#include <fenv.h>

extern float _mydiv(float, float);

void main(void)
{
  float f;
  int oldround;

  oldround = fegetround();
  fesetround(FE_TOWARDZERO);
  f = _mydiv(1.0f, 10.0f);
  fesetround(oldround);

  printf("%.18f\n", f);
}

Ao compilar e executar esse código (será preciso especificar a libm com -lm, no gcc) você verá que a divisão de 1 por 10 será arredondada para o MENOR valor, ou melhor, em direção ao zero. Esse será o valor mais distante do verdadeiro, neste caso. Troque 1.0f por -1.0f e veja o que acontece…

Depois, modifique o método para FE_DOWNWARD (arredondamenteo no sentido de -\infty) e teste os dois casos (com 1.0f e -1.0f)… Note que o truncamento citado no artigo anterior é equivalente a FE_TOWARDZERO…

O que quero dizer aqui é que, tanto na programação quanto na matemática, às vezes existe mais de um jeito “correto” de fazer alguma coisa. Depende apenas da aplicação.

Pré processador e constantes

Um amigo me mostrou um problema interessante que, confesso, levei algum tempo para resolver. O fragmento de código abaixo simplesmente não compila, causando um erro de “invalid suffix ‘…’ on integer or constant“:

/* fragmento de keys.h */
...
#define OFFSET 0x1000
...
#define ALT_1  (0xf8+OFFSET)
#define ALT_2  (0xf9+OFFSET)
#define ALT_3  (0xfa+OFFSET)
#define ALT_4  (0xfb+OFFSET)
#define ALT_5  (0xfc+OFFSET)
#define ALT_6  (0xfd+OFFSET)
#define ALT_7  (0xfe+OFFSET)
#define ALT_8  (0xff+OFFSET)
#define ALT_9  (0x80+OFFSET)
#define ALT_0  (0x81+OFFSET)
...
/* fragmento de main.c */
...
int altconvert[] = {
  ALT_A,ALT_B,ALT_C,ALT_D,ALT_E,ALT_F,ALT_G,ALT_H,
  ALT_I,ALT_J,ALT_K,ALT_L,ALT_M,ALT_N,ALT_O,ALT_P,
  ALT_Q,ALT_R,ALT_S,ALT_T,ALT_U,ALT_V,ALT_W,ALT_X,
  ALT_Y,ALT_Z,ALT_0,ALT_1,ALT_2,ALT_3,ALT_4,ALT_5,
  ALT_6,ALT_7,ALT_8,ALT_9
};

O erro aparece na linha que define o símbolo ALT_7 para o pré processador:

keys.h:97:21: error: invalid suffix "+OFFSET" on integer constant
 #define ALT_7 (0xfe+OFFSET)
                ^
main.c:9:11: note: in expansion of macro ‘ALT_7’
  ALT_6,ALT_7,ALT_8,ALT_9
        ^

Meu amigo ficou se perguntando isso não seria um bug do compilador… E a pergunta que fiquei me fazendo é: Por que diabos as definições de ALT_1 até ALT-6 e de ALT_8 e ALT_9 não deram o mesmo problema?

A diferença entre ALT_6, ALT_7 e ALT_8 é apenas uma: ALT_7 é um valor em hexadecimal terminada em ‘e’ e as demais não… Qual é mesmo a maneira de declarar constantes que tenham um ‘e’? Ahhhhh… ponto flutuante:

float c = 1e+10;

Acontece que os valores associados a ponto flutuante têm que ser expressos em decimal. Não podem ser hexadecimais… A não ser que estejamos falando do padrão C99 e, neste caso, o caractere ‘e’ teria que ser trocado por ‘p’:

float c = 0xfp+10;

Note que o expoente continua tendo que ser decimal…

No caso apresentado, o símbolo ALT_7 é definido como a string ‘0xfe+0x1000’. O ‘e’, seguido do valor inteiro, indica uma constante em ponto flutuante, mas hexadecimal… dai o erro. Que faz parte da especificação da linguagem! Não se trata de um bug!

Existem duas soluções possíveis:

  1. Colocar os valores inteiros na definição dos símbolos entre colchetes;
  2. Colocar um espaço entre os valores e o operador + (ou -).

É bom lembrar que o pré processador faz apenas substituições léxicas. Quando definimos um macro, como em:

#define advance_ptr(p) { (p)++; }

O pré compilador não efetua a operação… em qualquer parte do código onde advance_ptr for encontrado ele apenas substituirá p pelo parâmetro passado no macro… É o compilador que avaliará a operação ++, mas apenas depois que o pré compilador terminar suas substituições!

Portanto, a dica aqui é simples: Não assuma que o pré compilador “compila” alguma coisa ou avalia expressões… Nope, ele nunca fará isso!… Apenas “substituirá”. E “compilador” só tem um… a ênfase no nome deve ser no “pré”…

GCC 6.1.1 (unstable?) versus clang 3.8

Um leitor me fez a gentileza de compilar o código do artigo anterior (este aqui) usando o GCC 6.1.1 e o clang 3.8. O argumento, é claro, é um velho conhecido de todo mundo: “Ahhh, mas você está usando versões velhas!”. Isso porque usei o GCC 4.9.2 e o clang 3.5. Bem, comparem o código gerado por esses novíssimos e, supostamente, melhores compiladores, na mesma ordem do artigo anterior, primeiro o gerado pelo GCC 6.1.1 (que ainda está em fase de regressões e acertos) e o clang 3.8 (supostamente stable):

;------ GCC 6.1.1 -----------
  align 4
strlcpy:
  movzx ecx,byte [rsi]
  mov   rax,rdi
  test  cl,cl
  jz    .strlcpy_exit
  test  rdx,rdx
  jz    .strlcpy_exit

  add   rdx,rsi
  mov   r8,rdi
  jmp   .L1

  align 4
.loop:
  cmp   rdx,rsi
  je    .strlcpy_exit2
.L1:
  add   r8,1
  add   rsi,1
  mov   byte [r8-1],cl
  movzx ecx,byte [rsi]
  test  cl,cl
  jne   .loop

.strlcpy_exit2:
  mov   byte [r8],0
  ; --- O GCC 4.9 coloca o .strlcpy_exit aqui!
  ret

.strlcpy_exit:  
  mov   r8,rax
  jmp   .strlcpy_exit2

O GCC 6.1.1 conseguiu piorar, no finalzinho, uma rotina que parecia quase ideal. Note que a partir do label .strlcpy_exit o código é inútil, já que RDI já está em RAX!

O clang 3.8 faz um trabalho ainda pior que o seu irmão mais velho. Vejam:

;------ clang 3.8 -----------
  align 16
strlcpy:
  test  rdx,rdx
  mov   rax,rdi       ; Copia RDI para RAX
  jz    .strlcpy_exit ; Se size == 0, sai.

  mov   r8b,[rsi]     ; R8B = *src;
  test  r8b,r8b       
  mov   rax,rdi       ; Copia RDI para RAX (2ª vez).
  jz    .strlcpy_exit ; Se *src == '\0', sai.

  mov   ecx,1
  sub   rcx,rdx       ; RCX = 1 - RDX
  inc   rsi           ; src++;
  mov   rax,rdi       ; Copia RDI para RAX (3ª vez).
  
  align 16  
.loop:
  mov   [rax],r8b     ; *dest = *src;
  inc   rax           ; src++ (src temporário em RAX);
  test  rcx,rcx       
  jz    .strlcpy_exit ; RCX == 0? sai.

  mov   r8b,[rsi]     ; R8B = *src;
  inc   rcx           
  inc   rsi           ; src++;
  test  r8b,r8b
  jne   .loop         ; Se R8B != 0, loop.

.strlcpy_exit:
  mov   [rax],0       ; *dest = '\0';
  mov   rax,rdi       ; Copia RDI para RAX (4ª vez!).
  ret

O código não mudou muito desde a versão 3.5, exceto pelo fato de que, agora, a nova versão faz mais duas cópias desnecessárias de RDI para RAX!!!

Sinto muito, mas essas versões mais novas não me impressionaram nem um pouco… Bem… talvez o GCC tenha impressionado um pouquinho: Afinal, para uma versão não finalizada ele fez até um bom trabalho (como o GCC 4.9 faria)…

Qual dos dois você prefere (2)?

Pros amadores, defensores do C++, eis a rotininha basica mais usada para introduzir uma linguagem ao estudante: Hello, world!. Dessa vez, ambas escritas em C e C++ e seus equivalentes em assembly. Me diz qual você acha que é melhor, ok?

;------ C code -----
; #include <stdio.h>
; void main(void) { puts("Hello"); }
;-------------------
  bits 64

  section .rodata
hellostr: db  'Hello',0

  section .text

  extern puts

  global main
main:
  mov   rdi,hellostr
  jmp   puts

Agora veja o código em C++… Note que os nomes das funções são tão grandes que a formatação para evitar a barra de scroll na parte de baixo da listagem não é possível…

;------- C++ code -----
; #include <iostream>
; int main() { std::cout << "Hello\n"; }
;----------------------
  bits 64

  section .rodata
hellostr: db  'Hello',0x0a,0

  section .text

  ; MACACOS ME MORDAM!
  ; Precisa mesmo de 7 símbolos externos para
  ; imprimir uma stringzinha?!
  extern _ZSt4cout
  extern _ZSlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc 
  extern _ZStL8_ioinit
  extern _ZNSt8ios_base4InitC1Ev
  extern _ZNSt8ios_base4InitD1Ev
  extern __dso_handle
  extern __cxa_atexit

  global main
main:
  sub   rsp,8
  mov   rsi,hellostr
  mov   rdi,_ZSt4cout
  call  _ZSlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  xor   eax,eax
  add   rsp,8
  ret

_GLOBAL__sub_I_main:
  sub   rsp,8
  mov   rdi,_ZStL8_ioinit
  call  _ZNSt8ios_base4InitC1Ev
  mov   rdx,__dso_handle
  mov   rsi,_ZStL8__ioinit
  mov   rdi,_ZNSt8ios_base4InitD1Ev
  add   rsp,8
  jmp   __cxa_atexit

Credo! E olha que usei a otimização máxima… E quanto a usar a otimização mínima?! Usando a chave -O0, obtemos algo assim para as duas funções:

; código em C:
main:
  push rbp
  mov  rbp,rsp
  mov  rdi,hellostr
  call puts
  pop  rbp
  ret

Agora, segure-se na cadeira ao ver o código criado pelo C++:

; Código em C++:
main:
  push rbp
  mov  rbp,rsp
  mov  rsi,hellostr
  mov  rdi,_ZSt4cout
  call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
  mov  eax,0    
  pop  rbp
  ret

_Z41__static_initialization_and_destruction_0ii:
  push rbp
  mov  rbp,rsp
  sub  rsp,16
  mov  [rbp-4],edi
  mov  [rbp-8],esi
  mov  dword [rbp-4],1
  jne  .L3
  cmp  dword [rbp-8],0xffff
  jne  .L3
  mov  rdi,_ZStL8__ioinit
  call _ZNSt8ios_base4InitC1Ev
  mov  rdx,__dso_handle
  mov  rsi,_ZStL8__ioinit
  mov  rdi,_ZNSt8ios_base4InitD1Ev
  call __cxa_atexit
.L3:
  leave
  ret

_GLOBAL__sub_I_main:
  push rbp
  mov  rbp,rsp
  mov  esi,0xffff
  mov  edi,1
  call _Z41__static_initialization_and_destruction_0ii
  pop  rbp
  ret

Caramba! Os mesmos 7 símbolos e mais uma função e ainda com uns 2 saltos condicionais!

Agora me diz: Os códigos finais, gerados pelo compilador C++ são ou não uma sopa de letrinhas? Se eu te der um código em assembly, gerado pelo g++, você seria capaz de entendê-lo? Olhando para esses códigos ai em cima, nas variantes C, a chamada a puts é bem evidente (Opa! Então estamos imprimindo uma string usando stdout!), mas não podemos dizer o mesmo dos códigos compilados C++…

Afinal, o que _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc significa? E essas funções estáticas adicionais? Com um pouco de experiência você saberá que essas funções adicionais são relacionadas ao destrutor da classe basic_ostream, mas que é confuso pacas, é…

‘continue’ não faz o que você imagina em loops ‘do…while’

Por causa de construções do tipo:

while (i < 10)
{
  if (!foo(i))
    continue;
  bar(&i);
}

Onde, se a função foo() retornar zero o código salta para o início do loop. Isso funciona do mesmo jeito num loop com for, mas e num do…while?

do {
  if (!foo(i))
    continue;
  bar(&i);
} while (i < 10);

Será que, enquanto foo() retornar zero, o loop será executado de novo? A resposta é: depende do valor de i!

O que continue faz é reavaliar a condição do loop (i < 10) e, se for satisfeita, o loop continua. Funciona da mesma forma que no while

Cuidado com continue!

Mais uma pra quem acha que é fácil “manter” códigos em C++

Topei com essa já tem um tempo. Explico aqui também porque não gosto muito de declarar funções que não tomam parâmetros ao estilo de C++…

Considere esse simples código-fonte:

//--- test.cc ---
#include <iostream>

struct S {
  int x;

  S() { x = 1; }
  S(S& s) { x = s.x; }
};

// Declara um objeto s do tipo S construindo-o
// através de uma referência a um objeto anônimo
// criado via construdor default.
//
// Ou será que não é isso?
S s(S());

int main(void)
{
  std::cout << s.x << '\n';
}

Veja que s é declarado chamando o construtor S(S&) e como parâmetro desse construtor usamos S(), criando um objeto anônimo da classe S… Lembrando que um objeto anônimo é um que foi criado sem um nome (lógico, né?) e será destruído automaticamente logo após seu uso…

Agora, dê uma olhada na seguinte declaração:

int f();

Isso é o protótipo de uma função f que não toma parâmetros e devolve um inteiro. Não definimos a função, apenas a declaramos… Repare que, se tivéssemos um parâmetro para f, poderíamos ignorar o nome na declaração, usando apenas o tipo, como em:

int f(int);

Da mesma forma, ao declarar S(), dentro dos parênteses na declaração S s(S()) faz com que o compilador assuma que esse S() seja uma declaração de um ponteiro para uma função sem um nome. De forma mais explícita, o compilador deveria aceitar apenas algo assim:

S s(S (*)());

Mas, ao omitir a notação de ponteiro o compilador continua entendendo que a declaração S() ainda é um parâmetro de s que é um ponteiro para uma função!

Ao compilar o programinha, obtemos isso:

$ g++ -c -o test.o test.cc
test.cc: In function ‘int main()’:
test.cc:19:18: error: request for member ‘x’ in ‘s’, which is of non-class type ‘S(S (*)())’
   std::cout << s.x << '\n';
                  ^

O que o compilador tá te dizendo é: “s não é um objeto, seu trouxa, porque ‘S(S(*)())’ também não é!”.

O problema todo está no fato de que C++ aceita a abreviação de “(void)” como “()” nas declarações de funções (e seus protótipos) e, daí, cria uma ambiguidade do que é uma declaração de função e o que é uma chamada implícita a um construtor default, num objeto anônimo!

Esse comportamento do parser do compilador C++ chama-se de “the most vexing parse ou “a análise mais irritante (ou vergonhosa)”… Isso faz parte da especificação de C++.

Por esse motivo, mesmo em C++, prefiro declarar explicitamente os tipos void em funções que não tomam parâmetros e evitar a instanciação implícita de objetos anônimos com chamadas a construtores default. Repare como declarei main, usando explicitamente o tipo void no parâmetro…

Outro conselho… Esqueça essa história que C++ é C com classes… Não é! C e C++ são linguagens com similaridades, mas não são a mesma coisa!