C Atributos

Às vezes você sabe uma coisa sobre o seu código que o compilador não tem como adivinhar: "o retorno desta função é importante, não jogue fora", "esta função é antiga, não use mais", "aqui no switch eu quero mesmo continuar no próximo case". Os atributos C23 são etiquetas entre colchetes duplos, como [[nodiscard]], que contam isso ao compilador. Em troca, ele avisa quando alguém usa o código do jeito errado.

Você vai aprender
  • O que é um atributo e onde se escreve [[…]].
  • [[nodiscard]]: avisar quando o retorno de uma função é ignorado.
  • [[maybe_unused]]: calar o aviso de "variável não usada" quando isso é de propósito.
  • [[deprecated]]: marcar algo como antigo, com uma mensagem para quem ainda usa.
  • [[fallthrough]]: dizer que a "queda" para o próximo case do switch é intencional.
  • [[noreturn]] e o antigo _Noreturn: funções que nunca voltam.
  • O __attribute__((…)) do gcc, que veio antes de tudo isso.

Os programas desta aula foram compilados com o gcc 15.2 na máquina da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Atributos produzem avisos, por isso o -Wall -Wextra é importante aqui (aula Avisos).


A ideia: etiquetas na caixa da mudança

Numa mudança, você escreve nas caixas: "FRÁGIL", "ESTE LADO PARA CIMA", "NÃO JOGAR FORA". A etiqueta não muda o que está dentro da caixa. Ela muda o jeito como os outros a tratam: o carregador lê "FRÁGIL" e toma cuidado.

Atributos são essas etiquetas. [[nodiscard]] é o "NÃO JOGAR FORA" de uma função: o programa funciona igual, mas o compilador passa a reclamar de quem joga o retorno fora. Se o compilador não conhece uma etiqueta, ele a ignora (com um aviso), em vez de dar erro.

AtributoEtiqueta da caixaOnde vai
[[nodiscard]]"Não jogue o resultado fora"Função
[[maybe_unused]]"Pode ficar sem uso, está tudo bem"Variável, parâmetro, função
[[deprecated]]"Antigo: use outro"Função, variável, tipo
[[fallthrough]]"Continua no próximo, de propósito"Dentro do switch
[[noreturn]]"Daqui não se volta"Função

1. [[nodiscard]]: não jogue o retorno fora

"Discard" é "descartar"; no discard, "não descarte". Veja um erro comum: a função dobro devolve o resultado, mas quem chamou achou que ela mudaria a variável:

#include <stdio.h>

[[nodiscard]] int dobro(int x) {
    return x * 2;
}

int main(void) {
    int preco = 5;
    dobro(preco);
    printf("Preço: %d\n", preco);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra nodiscard.c -o nodiscard
nodiscard.c: In function ‘main’:
nodiscard.c:9:5: warning: ignoring return value of ‘dobro’, declared with attribute ‘nodiscard’ [-Wunused-result]
    9 |     dobro(preco);
      |     ^~~~~~~~~~~~
nodiscard.c:3:19: note: declared here
    3 | [[nodiscard]] int dobro(int x) {
      |                   ^~~~~
$ ./nodiscard
Preço: 5                  « o dobro foi calculado e jogado fora
[[nodiscard]]
O atributo, entre dois pares de colchetes, antes da declaração da função.
int dobro(int x)
A função continua igual. O atributo só muda o que o compilador confere em quem a chama.
warning: ignoring return value of ‘dobro’
"Ignorando o valor de retorno de dobro": a linha 9 chamou a função e não guardou nem usou o resultado.
declared with attribute ‘nodiscard’
"Declarada com o atributo nodiscard": o aviso existe por causa da etiqueta.
[-Wunused-result]
O nome do aviso ("resultado não usado"). Ele vem ligado mesmo sem -Wall.

Sem o [[nodiscard]], o gcc compilaria em silêncio e a Ana só descobriria o erro olhando a saída. O conserto é guardar o resultado: preco = dobro(preco);.

Com um motivo

Você pode explicar o porquê, entre parênteses. A mensagem aparece no aviso:

#include <stdio.h>
#include <stdlib.h>

[[nodiscard("o novo saldo vem no retorno")]]
int sacar(int saldo, int valor) {
    return saldo - valor;
}

int main(void) {
    int saldo = 100;
    sacar(saldo, 30);
    printf("Saldo: %d\n", saldo);
    malloc(10);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra nodiscard2.c -o nodiscard2
nodiscard2.c: In function ‘main’:
nodiscard2.c:11:5: warning: ignoring return value of ‘sacar’, declared with attribute ‘nodiscard’: "o novo saldo vem no retorno" [-Wunused-result]
   11 |     sacar(saldo, 30);
      |     ^~~~~~~~~~~~~~~~
nodiscard2.c:5:5: note: declared here
    5 | int sacar(int saldo, int valor) {
      |     ^~~~~
nodiscard2.c:13:5: warning: ignoring return value of ‘malloc’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
   13 |     malloc(10);
      |     ^~~~~~~~~~

Repare no segundo aviso: o malloc da biblioteca já vem com uma etiqueta parecida, warn_unused_result (a versão do gcc, mais antiga). Chamar malloc e jogar fora o endereço é sempre um erro: a memória fica reservada e ninguém consegue liberá-la (aula malloc e free).

O atributo pode ficar na mesma linha da função ou na linha de cima, como aqui: as duas formas são iguais.

Quando ignorar é de propósito

Se você quer mesmo descartar o resultado, escreva (void) antes da chamada. É um jeito de dizer "eu sei, e não quero":

    (void) dobro(5);      // compila sem aviso

No Clang, o compilador do editor do guia, o aviso é ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]: a mesma ideia, sem o nome da função.

Missão: rode e leia o aviso. O desconto de 10% foi calculado e jogado fora, e o preço continuou 50. Conserte a chamada para guardar o retorno em preco, até sair Preço: 45 sem nenhum aviso.

#include <stdio.h>

[[nodiscard]] int aplicar_desconto(int preco, int porcento) {
    return preco - preco * porcento / 100;
}

int main(void) {
    int preco = 50;
    aplicar_desconto(preco, 10);
    printf("Preço: %d\n", preco);
    return 0;
}
#include <stdio.h>

[[nodiscard]] int aplicar_desconto(int preco, int porcento) {
    return preco - preco * porcento / 100;
}

int main(void) {
    int preco = 50;
    preco = aplicar_desconto(preco, 10);
    printf("Preço: %d\n", preco);
    return 0;
}

2. [[maybe_unused]]: sem uso, de propósito

O -Wall -Wextra avisa sobre variáveis e parâmetros que nunca são usados, porque quase sempre isso é um esquecimento:

#include <stdio.h>

void saudar(int vezes, const char *nome) {
    int teste = 0;
    for (int i = 0; i < vezes; i++) {
        printf("Olá!\n");
    }
}

int main(void) {
    saudar(2, "Ana");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra unused0.c -o unused0
unused0.c: In function ‘saudar’:
unused0.c:4:9: warning: unused variable ‘teste’ [-Wunused-variable]
    4 |     int teste = 0;
      |         ^~~~~
unused0.c:3:36: warning: unused parameter ‘nome’ [-Wunused-parameter]
    3 | void saudar(int vezes, const char *nome) {
      |                        ~~~~~~~~~~~~^~~~

Unused variable = "variável não usada"; unused parameter = "parâmetro não usado". Às vezes, porém, é de propósito: a função ainda está pela metade, ou ela precisa ter aquele parâmetro porque vai ser guardada num ponteiro para função com um formato fixo (aula Ponteiros para Funções). Nesses casos, a etiqueta [[maybe_unused]] ("talvez não usado") cala o aviso só ali:

void saudar(int vezes, [[maybe_unused]] const char *nome) {
    [[maybe_unused]] int teste = 0;
    for (int i = 0; i < vezes; i++) {
        printf("Olá!\n");
    }
}
$ gcc -std=c23 -Wall -Wextra unused.c -o unused     « nenhum aviso

Antes do C23 o costume era escrever (void) nome; dentro da função, que "usa" o parâmetro sem fazer nada. Você ainda vai ver muito isso. Se a variável é só sobra (como teste), o melhor conserto é apagá-la; o [[maybe_unused]] é para quando ela precisa ficar.

Missão: a função tratar tem o formato que o sistema de botões da lojinha exige, por isso o parâmetro codigo precisa existir, mesmo sem uso. Rode e veja o aviso. Cale-o com [[maybe_unused]] no parâmetro (sem apagá-lo).

#include <stdio.h>

void tratar(int codigo) {
    printf("Olá! Botão apertado.\n");
}

int main(void) {
    tratar(7);
    return 0;
}
#include <stdio.h>

void tratar([[maybe_unused]] int codigo) {
    printf("Olá! Botão apertado.\n");
}

int main(void) {
    tratar(7);
    return 0;
}

3. [[deprecated]]: "isto é antigo, use outro"

"Deprecated" quer dizer obsoleto, "desaconselhado". Quando a Ana escreve uma versão melhor de uma função mas não pode apagar a velha (outras partes do programa ainda a usam), ela marca a velha:

#include <stdio.h>

[[deprecated("use calcular_total_com_desconto")]]
double calcular_total(double preco, int qtd) {
    return preco * qtd;
}

[[deprecated]] int versao_antiga = 1;

int main(void) {
    printf("%.2f\n", calcular_total(2.5, 4));
    printf("%d\n", versao_antiga);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra deprecated.c -o deprecated
deprecated.c: In function ‘main’:
deprecated.c:11:5: warning: ‘calcular_total’ is deprecated: use calcular_total_com_desconto [-Wdeprecated-declarations]
   11 |     printf("%.2f\n", calcular_total(2.5, 4));
      |     ^~~~~~
deprecated.c:4:8: note: declared here
    4 | double calcular_total(double preco, int qtd) {
      |        ^~~~~~~~~~~~~~
deprecated.c:12:5: warning: ‘versao_antiga’ is deprecated [-Wdeprecated-declarations]
   12 |     printf("%d\n", versao_antiga);
      |     ^~~~~~
deprecated.c:8:20: note: declared here
    8 | [[deprecated]] int versao_antiga = 1;
      |                    ^~~~~~~~~~~~~
$ ./deprecated
10.00
1
‘calcular_total’ is deprecated
"calcular_total está obsoleta". É só um aviso: o programa foi gerado e funciona.
: use calcular_total_com_desconto
A sua mensagem, dizendo o que usar no lugar. Sem mensagem (como no versao_antiga), o aviso só diz que é obsoleto.
note: declared here
Onde está a declaração marcada.

Repare que o aviso aparece em quem usa, não na declaração. Assim cada parte do programa que ainda depende da função velha fica marcada, e a Ana pode trocá-las aos poucos. Bibliotecas usam muito isso para avisar que algo vai sumir numa versão futura.


4. [[fallthrough]]: continuar no próximo case

Num switch, sem break, o programa "cai" no case de baixo (aula switch). Quase sempre isso é um break esquecido, e o -Wextra do gcc avisa. Mas às vezes cair é exatamente o que se quer. Na gincana da escola, quem chega ao nível 3 ganha os prêmios dos níveis 3, 2 e 1:

#include <stdio.h>

int main(void) {
    int nivel = 2;
    switch (nivel) {
    case 3:
        printf("Prêmio: viagem\n");
    case 2:
        printf("Prêmio: bicicleta\n");
    case 1:
        printf("Prêmio: chaveiro\n");
        break;
    default:
        printf("Sem prêmio\n");
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra fall.c -o fall
fall.c: In function ‘main’:
fall.c:7:9: warning: this statement may fall through [-Wimplicit-fallthrough=]
    7 |         printf("Prêmio: viagem\n");
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~
fall.c:8:5: note: here
    8 |     case 2:
      |     ^~~~
fall.c:9:9: warning: this statement may fall through [-Wimplicit-fallthrough=]
    9 |         printf("Prêmio: bicicleta\n");
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fall.c:10:5: note: here
   10 |     case 1:
      |     ^~~~
$ ./fall
Prêmio: bicicleta
Prêmio: chaveiro

This statement may fall through = "este comando pode cair (para o próximo)", e a nota here mostra para onde. O programa está certo, mas o gcc não tem como saber que a queda é proposital. Com [[fallthrough]]; no fim do case, você diz isso a ele (e a quem ler o código):

    switch (nivel) {
    case 3:
        printf("Prêmio: viagem\n");
        [[fallthrough]];
    case 2:
        printf("Prêmio: bicicleta\n");
        [[fallthrough]];
    case 1:
        printf("Prêmio: chaveiro\n");
        break;
    default:
        printf("Sem prêmio\n");
    }
$ gcc -std=c23 -Wall -Wextra fall2.c -o fall2     « nenhum aviso
[[fallthrough]]
"Cair através": a queda é intencional.
;
Este atributo fica sozinho, como um comando vazio, e por isso termina com ponto e vírgula.
logo antes do próximo case
Ele só pode aparecer como o último comando antes de um case ou default. Em outro lugar, é erro.
No editor do guia

O Clang não liga esse aviso com -Wall -Wextra: o fall.c compila calado no editor. Para vê-lo, é preciso pedir -Wimplicit-fallthrough, como faz a missão final desta aula. Aí o Clang avisa unannotated fall-through between switch labels ("queda sem anotação entre rótulos do switch") e até sugere: insert '[[fallthrough]];' to silence this warning.


5. [[noreturn]]: funções que nunca voltam

Algumas funções nunca devolvem o controle para quem as chamou: elas encerram o programa (com exit, aula Encerrar o Programa). O compilador não sabe disso, e essa ignorância gera um aviso falso:

#include <stdio.h>
#include <stdlib.h>

void erro_fatal(const char *msg) {
    fprintf(stderr, "Erro: %s\n", msg);
    exit(1);
}

int ler_idade(int idade) {
    if (idade >= 0) {
        return idade;
    }
    erro_fatal("idade negativa");
}

int main(void) {
    printf("%d\n", ler_idade(29));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra noreturn0.c -o noreturn0
noreturn0.c: In function ‘ler_idade’:
noreturn0.c:14:1: warning: control reaches end of non-void function [-Wreturn-type]
   14 | }
      | ^

"O controle chega ao fim de uma função não-void": o gcc acha que, depois de erro_fatal, a ler_idade termina sem return (aula Valor de Retorno). Marcando erro_fatal com [[noreturn]] ("não retorna"), o aviso some, porque agora o compilador sabe que nada depois dela vai rodar:

#include <stdio.h>
#include <stdlib.h>

[[noreturn]] void erro_fatal(const char *msg) {
    fprintf(stderr, "Erro: %s\n", msg);
    exit(1);
}

int ler_idade(int idade) {
    if (idade >= 0) {
        return idade;
    }
    erro_fatal("idade negativa");
}

int main(void) {
    printf("%d\n", ler_idade(29));
    printf("%d\n", ler_idade(-1));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra noreturn.c -o noreturn
$ ./noreturn
29
Erro: idade negativa
$ echo $?
1

E se ela voltar?

Prometer que não volta e voltar é grave. Sem o exit:

$ cat noreturn2.c
#include <stdio.h>

[[noreturn]] void erro_fatal(const char *msg) {
    fprintf(stderr, "Erro: %s\n", msg);
}

int main(void) {
    erro_fatal("teste");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra noreturn2.c -o noreturn2
noreturn2.c: In function ‘erro_fatal’:
noreturn2.c:5:1: warning: ‘noreturn’ function does return
    5 | }
      | ^
$ ./noreturn2
Erro: teste
Instrução ilegal         (imagem do núcleo gravada) ./noreturn2
$ echo $?
132

"Função noreturn retorna": o gcc avisou, e o programa morreu com "Instrução ilegal" (código 132 = 128 + 4, o sinal SIGILL; aula Sinais). Como o compilador confiou na promessa, ele não preparou nada para o caminho de volta. É comportamento indefinido: no editor do guia, o mesmo programa termina com "Falha de segmentação" (139). Nunca ignore esse aviso.

O antigo _Noreturn

Antes do C23, a mesma etiqueta se escrevia _Noreturn void erro_fatal(…) C11, ou noreturn com #include <stdnoreturn.h>. O gcc 15 ainda aceita as duas, sem aviso, até em C23. Mas o C23 marcou essas formas como obsoletas: em código novo, use [[noreturn]]. As funções exit e abort da biblioteca já vêm marcadas assim.


6. Formas diferentes: __attribute__ e outros atributos

Muito antes do C23, o gcc já tinha as suas etiquetas, com outra escrita: __attribute__((…)), com dois parênteses. Você vai encontrar isso em todo código C antigo e nos próprios cabeçalhos do sistema:

C23gcc (antigo)
[[nodiscard]]__attribute__((warn_unused_result))
[[maybe_unused]]__attribute__((unused))
[[deprecated("msg")]]__attribute__((deprecated("msg")))
[[fallthrough]]__attribute__((fallthrough)) ou o comentário /* fall through */
[[noreturn]]__attribute__((noreturn))

Alguns atributos do gcc não têm equivalente no padrão. Um muito útil é o format, que faz o compilador conferir os %d e %s de uma função sua parecida com o printf (aula Funções Variádicas). No C23, ele pode ser escrito com colchetes e o "sobrenome" do compilador, [[gnu::format(printf, 1, 2)]]:

$ cat gnu.c
#include <stdio.h>

[[gnu::format(printf, 1, 2)]]
void registrar(const char *formato, ...);

int main(void) {
    registrar("%d itens\n", "três");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -c gnu.c          « -c: só compila, sem ligar
gnu.c: In function ‘main’:
gnu.c:7:17: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Wformat=]
    7 |     registrar("%d itens\n", "três");
      |                ~^           ~~~~~~
      |                 |           |
      |                 int         char *
      |                %s

O 1, 2 quer dizer "o formato é o 1º parâmetro, e os valores começam no 2º". Agora a função da Ana ganhou a mesma vigilância do printf.

O C23 também criou [[unsequenced]] e [[reproducible]], que prometem que uma função não tem efeitos colaterais, para o compilador otimizar. O gcc 15 os conhece (e exige que fiquem depois dos parênteses dos parâmetros: int quadrado(int x) [[unsequenced]];); o Clang do editor ainda os ignora com o aviso unknown attribute 'unsequenced' ignored. São recursos avançados, raros em código do dia a dia.

Qual usar

  • Em código novo (C23), use os atributos com colchetes. São padrão e funcionam no gcc e no Clang.
  • [[nodiscard]] vale ouro em funções que devolvem um resultado que precisa ser usado: um código de erro, memória reservada, um valor novo calculado.
  • [[maybe_unused]] e [[fallthrough]] servem para manter o -Wall -Wextra sem avisos falsos. Aviso falso que fica na tela esconde os verdadeiros.
  • __attribute__ só quando precisar de um recurso do gcc que não tem versão padrão (prefira então a forma [[gnu::…]]).

Quando dá errado

warning: ‘importante’ attribute ignored [-Wattributes]

Aparece com um atributo que o compilador não conhece, como [[importante]] int x = 1; (ou com um nome escrito errado, como [[nodiscart]]). O Clang diz unknown attribute 'importante' ignored.

O que significa: a etiqueta foi ignorada. O programa compila, mas sem a proteção que você queria.

Como consertar: confira a grafia. Os atributos padrão são nodiscard, maybe_unused, deprecated, fallthrough, noreturn, unsequenced e reproducible.

warning: ignoring return value of ‘f’, declared with attribute ‘nodiscard’

O que significa: você chamou a função e não usou o resultado. Quase sempre é um erro de lógica: o resultado era a resposta.

Como consertar: guarde (x = f(x);) ou use o resultado (num if, num printf). Se descartar for mesmo intencional, escreva (void) f(x);.

warning: this statement may fall through [-Wimplicit-fallthrough=]

O que significa: um case termina sem break e o programa vai continuar no próximo.

Como consertar: se foi esquecimento, ponha break;. Se é de propósito, ponha [[fallthrough]]; como último comando do case.

warning: ‘noreturn’ function does return

O que significa: uma função marcada [[noreturn]] tem um caminho que chega ao fim. Se esse caminho acontecer, o programa pode morrer de forma estranha (vimos "Instrução ilegal").

Como consertar: garanta que todo caminho termine com exit, abort ou outra função [[noreturn]]. Se a função às vezes volta, tire o atributo.


Referência rápida

AtributoOndeO que faz
[[nodiscard]] / [[nodiscard("motivo")]]FunçãoAvisa (-Wunused-result) quando o retorno é ignorado
[[maybe_unused]]Variável, parâmetro, funçãoCala os avisos de "não usado"
[[deprecated]] / [[deprecated("use x")]]Função, variável, tipoAvisa (-Wdeprecated-declarations) em cada uso
[[fallthrough]];Último comando de um caseDiz que a queda para o próximo case é intencional
[[noreturn]]FunçãoPromete que a função nunca volta (substitui _Noreturn)
(void) f(x);ChamadaDescarta o retorno de propósito, sem aviso
[[gnu::format(printf, 1, 2)]]FunçãoAtributo do gcc: confere o formato como no printf
__attribute__((…))VáriosA escrita antiga do gcc

Exercícios

1. Não jogue fora

Complete para o compilador avisar quando alguém ignorar o retorno de calcular:

[[]] int calcular(int x);

2. Queda intencional

Complete a linha que vai no fim do case para dizer que cair no próximo é de propósito:

3. O que acontece quando você usa uma função marcada com [[deprecated]]?

4. A função f é [[nodiscard]]. Qual destas linhas compila sem aviso?

5. Uma função marcada [[noreturn]] chega ao fim e volta. O que acontece?

6. Qual era a forma do C11 para "esta função não retorna"?

Missão final: os prêmios da gincana

Missão: esta caixa compila com -Wimplicit-fallthrough, então o Clang avisa sobre as quedas entre os case. Quem chega ao nível 3 deve ganhar os três prêmios, então as quedas são de propósito. Marque-as com [[fallthrough]]; para o programa mostrar os três prêmios sem nenhum aviso (não use break nesses dois lugares).

Prêmio: viagem
Prêmio: bicicleta
Prêmio: chaveiro
#include <stdio.h>

int main(void) {
    int nivel = 3;
    switch (nivel) {
    case 3:
        printf("Prêmio: viagem\n");
    case 2:
        printf("Prêmio: bicicleta\n");
    case 1:
        printf("Prêmio: chaveiro\n");
        break;
    default:
        printf("Sem prêmio\n");
    }
    return 0;
}
#include <stdio.h>

int main(void) {
    int nivel = 3;
    switch (nivel) {
    case 3:
        printf("Prêmio: viagem\n");
        [[fallthrough]];
    case 2:
        printf("Prêmio: bicicleta\n");
        [[fallthrough]];
    case 1:
        printf("Prêmio: chaveiro\n");
        break;
    default:
        printf("Sem prêmio\n");
    }
    return 0;
}

Resumo

  • Atributos C23 são etiquetas [[nome]] que contam ao compilador algo que ele não adivinha. Eles não mudam o que o programa faz; mudam os avisos.
  • [[nodiscard]]: aviso quando o retorno é ignorado. (void) f(); descarta de propósito.
  • [[maybe_unused]]: cala "unused variable/parameter" quando a falta de uso é intencional.
  • [[deprecated("use x")]]: aviso em cada uso de algo antigo.
  • [[fallthrough]];: a queda para o próximo case é proposital (o gcc avisa com -Wextra; o Clang só com -Wimplicit-fallthrough).
  • [[noreturn]]: a função nunca volta. Substitui o _Noreturn do C11, que o C23 marcou como obsoleto. Voltar mesmo assim é comportamento indefinido.
  • O gcc tem a escrita antiga __attribute__((…)) e atributos próprios, como [[gnu::format(printf, 1, 2)]].