C Funções inline
Chamar uma função tem um pequeno custo: o programa para o que está fazendo, pula para outro lugar, faz a conta e volta. Para uma função grande, esse custo some no meio do trabalho. Para uma função de uma linha, como "o dobro de um número", o pulo pode demorar mais que a conta. A palavra inline ("em linha") pede ao compilador: "em vez de pular até a função, copie o corpo dela no lugar da chamada". Nesta aula você vai ver o que isso faz de verdade, por que é só um pedido, e uma armadilha que existe no C e não existe no C++: o inline sozinho pode fazer o programa nem ligar.
- O que é expandir uma função em linha, olhando o código de máquina com
gcc -S. - Por que
inlineé uma dica, não uma ordem (e o que o-O2tem a ver com isso). - O erro de ligação do
inlinesozinho em C, com-O0, e por que no C++ ele não acontece. - O jeito certo de pôr uma função pequena num arquivo
.h:static inline. - A alternativa do C99, com
extern inline. - Macro × função
inline: quando cada uma serve.
Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/inline, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa (veja Compilar com gcc). Às vezes acrescentamos -O2, que liga as otimizações; sem ele vale -O0, sem otimizar. Esta diferença é o coração da aula. Antes, convém conhecer Funções e Arquivos .h e .c.
A ideia: "veja a página 42" ou copiar a receita
A Ana escreve um livro de receitas. Várias receitas usam a mesma calda de chocolate. Ela pode escrever a calda uma vez, na página 42, e nas outras receitas pôr "faça a calda (veja a página 42)". Quem cozinha precisa virar até a página 42, ler, e voltar para onde estava. Ou ela pode copiar a receita da calda dentro de cada receita que a usa.
| No livro | No programa | Vantagem | Desvantagem |
|---|---|---|---|
| "Veja a página 42" | Chamada normal de função: o programa pula até ela e volta | A receita existe uma vez só: livro fino | Vira a página a cada uso |
| Copiar a receita no lugar | Função expandida em linha (inlined): o corpo é copiado onde ela é chamada | Ninguém vira página: mais rápido | Se a receita é grande e aparece muito, o livro engorda |
Para uma receita de duas linhas, copiar vale a pena. Para uma de três páginas, não. Quem decide, no fim, é a "editora": o compilador.
1. O que o compilador faz numa chamada
Comece por uma função pequena, marcada com static inline (as duas palavras são explicadas nas seções 3 e 4):
#include <stdio.h>
static inline int dobro(int x) {
return x * 2;
}
int main(void) {
printf("O dobro de 21 é %d\n", dobro(21));
return 0;
}
$ gcc -std=c23 -Wall -Wextra dobro2.c -o dobro2 $ ./dobro2 O dobro de 21 é 42
Nada de especial na saída. A diferença está dentro do programa. A opção -S faz o gcc parar antes de montar o executável e mostrar o código de montagem (assembly): as instruções do processador, uma por linha (veja Como o C Vira Programa). Com -o -, ele escreve na tela, e o grep filtra só as linhas que interessam:
$ gcc -std=c23 -S -o - dobro2.c | grep -E 'call|dobro' .file "dobro2.c" .type dobro, @function dobro: « a função existe no programa .size dobro, .-dobro .string "O dobro de 21 \303\251 %d\n" call dobro « o main PULA até ela call printf@PLT
Sem otimização, o inline foi ignorado: a função dobro existe, e o main a chama com call dobro ("chame dobro"). Agora com -O2:
$ gcc -std=c23 -O2 -S -o - dobro2.c | grep -E 'call|dobro|42' .file "dobro2.c" .string "O dobro de 21 \303\251 %d\n" movl $42, %edx « o 42 já calculado! call __printf_chk@PLT
A função dobro sumiu, junto com o call dobro. O compilador copiou o corpo (x * 2) no lugar da chamada, viu que 21 * 2 é sempre 42 e escreveu o 42 direto (movl $42 = "ponha 42 no lugar do segundo argumento do printf"). É isso que "expandir em linha" permite: depois da cópia, o compilador enxerga mais e simplifica mais. (O __printf_chk é uma versão do printf com conferências extras que o Ubuntu usa quando há otimização.)
2. Uma dica, não uma ordem
O padrão do C diz que inline é uma sugestão: "chamadas a esta função devem ser tão rápidas quanto possível". O compilador pode ignorar. E ele também pode expandir em linha funções que não têm inline. Veja esta, sem a palavra:
#include <stdio.h>
static int dobro(int x) {
return x * 2;
}
int main(void) {
printf("O dobro de 21 é %d\n", dobro(21));
return 0;
}
$ gcc -std=c23 -O2 -S -o - dica.c | grep -E 'call|dobro|42' .string "O dobro de 21 \303\251 %d\n" movl $42, %edx « expandida em linha, mesmo sem "inline" call __printf_chk@PLT
O mesmo resultado. Os compiladores modernos decidem sozinhos, com base no tamanho da função e em quantas vezes ela é chamada, e decidem melhor do que a maioria das pessoas. Resumindo:
Com -O0 (padrão) | Com -O2 | |
|---|---|---|
Função com inline | Chamada normal (call) | Quase sempre expandida, se for pequena |
Função sem inline | Chamada normal (call) | Expandida também, se for pequena |
Então para que serve o inline hoje? Menos para velocidade e mais para organização: ele permite pôr a definição de uma função num arquivo .h, incluído por vários .c (seção 4). Se você precisa mesmo forçar a expansão, o gcc e o Clang têm o atributo [[gnu::always_inline]] (uma extensão, não faz parte do padrão; veja Atributos): com ele, o call dobro some até com -O0. Quase nunca é necessário.
Não espalhe inline pelo programa esperando que ele fique mais rápido. Compile com -O2 e deixe o compilador escolher. Se um trecho parecer lento, meça antes de mudar qualquer coisa.
3. A armadilha: inline sozinho em C
Até aqui, as funções tinham static inline. E se você escrever só inline, como faria em C++?
#include <stdio.h>
inline int dobro(int x) {
return x * 2;
}
int main(void) {
printf("O dobro de 21 é %d\n", dobro(21));
return 0;
}
$ gcc -std=c23 -Wall -Wextra dobro.c -o dobro /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccTKYlLl.o: na função "main": dobro.c:(.text+0xe): referência não definida para "dobro" collect2: error: ld returned 1 exit status $ gcc -std=c23 -Wall -Wextra -O2 dobro.c -o dobro $ ./dobro O dobro de 21 é 42
Com o comando padrão, o programa não liga. A mensagem não vem do compilador, e sim do ligador (ld, o programa que junta os pedaços no executável; no laboratório ele fala português): referência não definida para "dobro" (em inglês, undefined reference to `dobro', como você vai ver em fóruns). O main chama uma dobro que não existe em lugar nenhum. Com -O2, compila e roda. Que história é essa?
O que o inline sozinho quer dizer em C
Desde o C99, uma função marcada só com inline num arquivo é uma definição para expandir, e nada mais. O compilador entende assim: "pode copiar este corpo nas chamadas; e, se não copiar, a versão de verdade da função (a definição externa) está em outro arquivo". O compilador não cria a versão de verdade.
- Com
-O2, o gcc expandiu a chamada em linha. Ninguém precisou da versão de verdade: ligou. - Com
-O0, o gcc não expande nada e deixou umcall dobro. O ligador procurou a versão de verdade em todos os arquivos, não achou, e parou.
Ou seja, o programa depende da otimização para ligar. É exatamente o que você não quer.
Em C++, uma função inline num cabeçalho gera uma cópia em cada arquivo que a usa, e o ligador fica com uma só. Por isso quem vem do C++ (ou leu código C++) escreve inline sozinho e se assusta com o erro. O C seguiu outro caminho. Para piorar, o gcc antigo, no modo -std=gnu89, usava uma terceira regra, em que o inline sozinho criava a versão de verdade (com -std=gnu89, o dobro.c liga até com -O0). É por isso que você encontra código antigo que funcionava e hoje não liga.
No editor do guia acontece o mesmo, com as palavras do ligador do WebAssembly: wasm-ld: error: … undefined symbol: dobro ("símbolo não definido: dobro"). E, como no gcc, com -O2 ele liga. Conserte na caixa abaixo:
Missão: rode e veja o erro do ligador. Conserte acrescentando uma palavra antes do inline, para a função ficar static inline e o programa mostrar O dobro de 21 é 42.
#include <stdio.h>
inline int dobro(int x) {
return x * 2;
}
int main(void) {
printf("O dobro de 21 é %d\n", dobro(21));
return 0;
}
#include <stdio.h>
static inline int dobro(int x) {
return x * 2;
}
int main(void) {
printf("O dobro de 21 é %d\n", dobro(21));
return 0;
}
Por que o static resolve? Uma função static é particular do arquivo em que está (veja static e extern). Então não existe "versão de verdade em outro arquivo" para procurar: o compilador cria a função ali mesmo, se precisar dela. Com -O0, ele cria e chama (o call dobro da seção 1); com -O2, expande em linha e a apaga.
4. static inline num arquivo .h
O uso mais comum de inline em C é este: uma função pequena, usada por vários arquivos .c, escrita inteira num .h. A lojinha da Ana calcula preços com desconto no programa principal e no relatório:
#ifndef PRECO_H
#define PRECO_H
static inline double com_desconto(double preco, double porcento) {
return preco - preco * porcento / 100.0;
}
#endif
#include <stdio.h>
#include "preco.h"
void relatorio(void);
int main(void) {
printf("Café com 10%% de desconto: R$ %.2f\n", com_desconto(8.00, 10));
relatorio();
return 0;
}
#include <stdio.h>
#include "preco.h"
void relatorio(void) {
printf("Bolo com 25%% de desconto: R$ %.2f\n", com_desconto(20.00, 25));
}
$ gcc -std=c23 -Wall -Wextra main.c relatorio.c -o loja $ ./loja Café com 10% de desconto: R$ 7.20 Bolo com 25% de desconto: R$ 15.00
- #ifndef PRECO_H … #endif
- A guarda de inclusão: impede que o conteúdo entre duas vezes no mesmo
.c(veja Arquivos .h e .c). - static
- Cada
.cque inclui opreco.hganha a sua própria cópia particular da função. Omain.ce orelatorio.cnão brigam pelo nome. - inline
- A dica de expandir. Ela também evita um aviso: com
staticsozinho, um.cque incluísse o.hsem usar a função levaria do gcc umwarning: ‘com_desconto’ defined but not used [-Wunused-function]("definida mas não usada"). Comstatic inline, o gcc não reclama. - %%
- Dentro do
printf,%%escreve um%(veja Formatos do printf).
Experimente: os três arquivos estão nas abas. Rode. Depois apague a palavra static no preco.h e rode de novo: o ligador vai reclamar de com_desconto nos dois arquivos. Depois apague também o inline (fica só double com_desconto) e veja um terceiro erro.
#include <stdio.h>
#include "preco.h"
void relatorio(void);
int main(void) {
printf("Café com 10%% de desconto: R$ %.2f\n", com_desconto(8.00, 10));
relatorio();
return 0;
}
#include <stdio.h>
#include "preco.h"
void relatorio(void) {
printf("Bolo com 25%% de desconto: R$ %.2f\n", com_desconto(20.00, 25));
}
#ifndef PRECO_H
#define PRECO_H
static inline double com_desconto(double preco, double porcento) {
return preco - preco * porcento / 100.0;
}
#endif
Os três jeitos de escrever a função no .h, com dois arquivos .c e o comando padrão:
Sem nada: double com_desconto(…)
$ gcc -std=c23 -Wall -Wextra main.c relatorio.c -o loja /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cctIVCDK.o: na função "com_desconto": relatorio.c:(.text+0x0): múltiplas definições de "com_desconto"; /tmp/ccarBf93.o:main.c:(.text+0x0): definido primeiro aqui collect2: error: ld returned 1 exit status
Cada .c criou a sua versão de verdade, pública, com o mesmo nome. O ligador achou duas: múltiplas definições de "com_desconto" (multiple definition of), definido primeiro aqui (first defined here).
Só inline: inline double com_desconto(…)
$ gcc -std=c23 -Wall -Wextra main.c relatorio.c -o loja /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccRsxu4F.o: na função "main": main.c:(.text+0x21): referência não definida para "com_desconto" /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccYMnTJv.o: na função "relatorio": relatorio.c:(.text+0x21): referência não definida para "com_desconto" collect2: error: ld returned 1 exit status $ gcc -std=c23 -Wall -Wextra -O2 main.c relatorio.c -o loja $ ./loja Café com 10% de desconto: R$ 7.20 Bolo com 25% de desconto: R$ 15.00
O oposto: nenhum arquivo criou a versão de verdade (a armadilha da seção 3, agora em dois arquivos). Com -O2, liga, por sorte da otimização.
static inline double com_desconto(…)
Funciona sempre, com qualquer otimização (a saída do começo da seção).
No .h | Quantas versões de verdade | Resultado com -O0 |
|---|---|---|
double f(…) { … } | Uma por .c, todas públicas | múltiplas definições de "f" |
inline double f(…) { … } | Nenhuma | referência não definida para "f" |
static inline double f(…) { … } | Uma particular por .c, só se precisar | Funciona |
5. A alternativa do C99: inline no .h e extern inline num .c
O padrão prevê outro jeito, que evita as cópias particulares: deixe o inline sozinho no .h e crie, num único .c, a versão de verdade, com uma declaração extern inline:
#ifndef PRECO_H
#define PRECO_H
inline double com_desconto(double preco, double porcento) {
return preco - preco * porcento / 100.0;
}
#endif
#include "preco.h" // a única definição "de verdade", para o ligador achar extern inline double com_desconto(double preco, double porcento);
$ gcc -std=c23 -Wall -Wextra main.c relatorio.c preco.c -o loja $ ./loja Café com 10% de desconto: R$ 7.20 Bolo com 25% de desconto: R$ 15.00
- extern inline double com_desconto(…);
- Só uma declaração, sem corpo. Mas, como o corpo
inlineveio do.hlogo acima, a palavraexternmanda o compilador criar aqui a versão de verdade, pública. Vale escrever sódouble com_desconto(double, double);também: qualquer declaração seminlinetem o mesmo efeito. - preco.c
- Tem de entrar na linha do
gcc(ou noMakefile; veja Makefile). Se ficar de fora, volta o erroreferência não definida.
Funciona com -O0 e com -O2, e o programa tem uma única cópia da função para quem não for expandido. É o jeito "oficial" do C99, comum em bibliotecas grandes. Mas exige um arquivo a mais e é fácil de esquecer.
6. Macro × função inline
Antes do C99, o único jeito de ter "uma função copiada no lugar" era uma macro com parâmetros (veja Macros com Parâmetros). O pré-processador troca o texto antes de compilar, sem pulo nenhum. Parece igual, mas não é:
#include <stdio.h>
#define QUADRADO(x) x * x
static inline int quadrado(int x) {
return x * x;
}
int main(void) {
printf("macro: %d\n", QUADRADO(2 + 3));
printf("inline: %d\n", quadrado(2 + 3));
return 0;
}
$ gcc -std=c23 -Wall -Wextra macro.c -o macro $ ./macro macro: 11 inline: 25
A macro trocou o texto: QUADRADO(2 + 3) virou 2 + 3 * 2 + 3, que dá 2 + 6 + 3 = 11 (a multiplicação vem primeiro; veja Precedência). A função recebeu o valor 5 e devolveu 25. Os parênteses consertam esse caso, #define QUADRADO(x) ((x) * (x)), mas não este:
#include <stdio.h>
#define QUADRADO(x) ((x) * (x))
static inline int quadrado(int x) {
return x * x;
}
int main(void) {
int a = 3;
int r1 = QUADRADO(a++);
printf("macro: %d (a agora vale %d)\n", r1, a);
int b = 3;
int r2 = quadrado(b++);
printf("inline: %d (b agora vale %d)\n", r2, b);
return 0;
}
$ gcc -std=c23 -Wall -Wextra macro2.c -o macro2
macro2.c: In function ‘main’:
macro2.c:11:24: warning: operation on ‘a’ may be undefined [-Wsequence-point]
11 | int r1 = QUADRADO(a++);
| ^
macro2.c:3:29: note: in definition of macro ‘QUADRADO’
3 | #define QUADRADO(x) ((x) * (x))
| ^
$ ./macro2
macro: 12 (a agora vale 5)
inline: 9 (b agora vale 4)
A macro virou ((a++) * (a++)): o a++ aconteceu duas vezes, o resultado foi 12 (nem 9, nem 16) e o gcc avisou que isso é comportamento indefinido (veja Comportamento Indefinido). A função calculou b++ uma vez só, como qualquer função: recebeu 3, devolveu 9, e b foi para 4.
A macro tem uma vantagem: não tem tipo. QUADRADO(1.5) dá 2.25, enquanto quadrado(1.5) converte o 1.5 para o int 1 (e o -Wconversion avisa: conversion from ‘double’ to ‘int’ changes value from ‘1.5e+0’ to ‘1’). Para uma função que aceite vários tipos sem os perigos da macro, o C11 tem o _Generic.
Macro #define F(x) | Função static inline | |
|---|---|---|
| Quem trata | O pré-processador, trocando texto | O compilador, como uma função normal |
| Argumento calculado | Uma vez para cada vez que aparece (a++ duas vezes!) | Uma vez só |
| Precedência | Precisa de parênteses em tudo | Sem surpresa |
| Tipos | Não confere nada; aceita qualquer tipo | Confere e converte, com avisos |
| Erros do compilador | Apontam para o texto já trocado (difíceis de ler) | Apontam para a função |
| Depurador (gdb) | A macro não existe para ele | Dá para parar dentro (com -O0) |
Velocidade com -O2 | A mesma | A mesma |
Formas diferentes: qual usar
| Situação | Use |
|---|---|
Função usada só num .c | static (com ou sem inline): com -O2, o compilador expande sozinho se valer a pena |
Função pequena (poucas linhas), usada por vários .c | static inline no .h |
Função grande, usada por vários .c | Uma função normal: protótipo no .h, corpo num .c (veja Vários Arquivos) |
| Uma única cópia de verdade da função pequena | inline no .h + extern inline num .c |
Algo que funções não fazem (trocar texto, __FILE__, __LINE__) | Macro |
| Nunca | inline sozinho num .c (não liga com -O0) |
Quando dá errado
referência não definida para "f" (undefined reference to `f'), com uma função inline
O que significa: a função está marcada só com inline. Em C, isso não cria a versão de verdade; com -O0, o compilador chama a função em vez de expandir, e o ligador não a encontra. No editor do guia: wasm-ld: error: … undefined symbol: f.
Como consertar: troque por static inline. Ou, se quiser uma única cópia, ponha extern inline + o protótipo num .c e inclua esse .c na compilação.
-O2, mas não com -O0"
O que significa: quase sempre é o mesmo problema do inline sozinho: com -O2, todas as chamadas foram expandidas e ninguém precisou da versão de verdade.
Como consertar: como acima. Um programa tem de ligar com qualquer otimização.
múltiplas definições de "f" (multiple definition of `f')
O que significa: uma função com corpo, sem static nem inline, está num .h incluído por dois ou mais .c. Cada um criou a sua versão pública, e o ligador achou duas.
Como consertar: se a função é pequena, static inline. Se é grande, deixe no .h só o protótipo (double f(double);) e mova o corpo para um .c.
11 em vez de 25)
O que significa: a macro trocou o texto sem parênteses, e a precedência mudou a conta. Ou o argumento tem ++ e foi calculado duas vezes (o gcc avisa operation on ‘a’ may be undefined, e o Clang, multiple unsequenced modifications to 'a').
Como consertar: troque a macro por uma função static inline. Se precisar manter a macro, ponha parênteses em cada uso do parâmetro e em volta de tudo, e nunca passe a++ para ela.
Referência rápida
| Escrita | O que significa em C |
|---|---|
static inline int f(int x) { … } | Função particular do arquivo, com dica de expandir. O jeito seguro, inclusive em .h |
inline int f(int x) { … } | Definição só para expandir; a versão de verdade tem de existir em outro lugar |
extern inline int f(int x); | Num .c que já viu o corpo inline: cria aqui a versão de verdade |
static int f(int x) { … } | Função particular; com -O2, o compilador também pode expandir |
[[gnu::always_inline]] | Extensão do gcc e do Clang: expande sempre, até com -O0 |
gcc -S -o - arquivo.c | Mostra o código de montagem; call f = chamada, sem call = expandida |
#define F(x) ((x) * (x)) | Macro: troca de texto, sem tipo, argumento repetido |
Exercícios
1. O jeito seguro
Complete para a função poder ficar num .h incluído por vários .c, ligando com qualquer otimização:
2. Ver o código de máquina
Complete a opção do gcc que mostra o código de montagem em vez de criar o executável:
3. Um arquivo tem inline int dobro(int x) { return x * 2; } e o main chama dobro(21). Com gcc -std=c23 -Wall -Wextra (sem -O2), o que acontece?
4. O que é verdade sobre a palavra inline?
5. Com #define DOBRO(x) x + x, quanto vale DOBRO(3) * 10?
6. Uma função normal (sem static nem inline), com corpo, está num .h incluído por main.c e loja.c. O que o ligador diz?
Missão final: da macro para a função
Missão: a Ana quer saber a área de uma prateleira com lado + 1 de largura e 4 de altura, mas a macro AREA calcula errado. Troque a macro por uma função static inline int area(int largura, int altura) e use-a no printf, para a saída ser Área: 12.
#include <stdio.h>
#define AREA(l, a) l * a
int main(void) {
int lado = 2;
printf("Área: %d\n", AREA(lado + 1, 4));
return 0;
}
#include <stdio.h>
static inline int area(int largura, int altura) {
return largura * altura;
}
int main(void) {
int lado = 2;
printf("Área: %d\n", area(lado + 1, 4));
return 0;
}
Resumo
- Expandir em linha (inline) = copiar o corpo da função no lugar da chamada, sem o pulo. Dá para ver com
gcc -S: some ocall. inlineé uma dica. Com-O0, nada é expandido; com-O2, o compilador expande funções pequenas, com ou seminline.- Em C,
inlinesozinho não cria a versão de verdade da função: com-O0, dáreferência não definidano ligador. No C++ não é assim. - O jeito seguro é
static inline, inclusive em arquivos.h. A alternativa do C99 éinlineno.h+extern inlinenum único.c. - Prefira
static inlinea macros com parâmetros: o argumento é calculado uma vez, não há surpresa de precedência e os tipos são conferidos.