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.

Você vai aprender
  • 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 -O2 tem a ver com isso).
  • O erro de ligação do inline sozinho 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 livroNo programaVantagemDesvantagem
"Veja a página 42"Chamada normal de função: o programa pula até ela e voltaA receita existe uma vez só: livro finoVira a página a cada uso
Copiar a receita no lugarFunção expandida em linha (inlined): o corpo é copiado onde ela é chamadaNinguém vira página: mais rápidoSe 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 inlineChamada normal (call)Quase sempre expandida, se for pequena
Função sem inlineChamada 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.

Velocidade de verdade se mede

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 um call 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.

No C++ isso não acontece

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 .c que inclui o preco.h ganha a sua própria cópia particular da função. O main.c e o relatorio.c não brigam pelo nome.
inline
A dica de expandir. Ela também evita um aviso: com static sozinho, um .c que incluísse o .h sem usar a função levaria do gcc um warning: ‘com_desconto’ defined but not used [-Wunused-function] ("definida mas não usada"). Com static 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 .hQuantas versões de verdadeResultado com -O0
double f(…) { … }Uma por .c, todas públicasmúltiplas definições de "f"
inline double f(…) { … }Nenhumareferência não definida para "f"
static inline double f(…) { … }Uma particular por .c, só se precisarFunciona

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 inline veio do .h logo acima, a palavra extern manda o compilador criar aqui a versão de verdade, pública. Vale escrever só double com_desconto(double, double); também: qualquer declaração sem inline tem o mesmo efeito.
preco.c
Tem de entrar na linha do gcc (ou no Makefile; veja Makefile). Se ficar de fora, volta o erro referê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 trataO pré-processador, trocando textoO compilador, como uma função normal
Argumento calculadoUma vez para cada vez que aparece (a++ duas vezes!)Uma vez só
PrecedênciaPrecisa de parênteses em tudoSem surpresa
TiposNão confere nada; aceita qualquer tipoConfere e converte, com avisos
Erros do compiladorApontam para o texto já trocado (difíceis de ler)Apontam para a função
Depurador (gdb)A macro não existe para eleDá para parar dentro (com -O0)
Velocidade com -O2A mesmaA mesma

Formas diferentes: qual usar

SituaçãoUse
Função usada só num .cstatic (com ou sem inline): com -O2, o compilador expande sozinho se valer a pena
Função pequena (poucas linhas), usada por vários .cstatic inline no .h
Função grande, usada por vários .cUma função normal: protótipo no .h, corpo num .c (veja Vários Arquivos)
Uma única cópia de verdade da função pequenainline no .h + extern inline num .c
Algo que funções não fazem (trocar texto, __FILE__, __LINE__)Macro
Nuncainline 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.

"Compila com -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.

Uma macro dá um resultado esquisito (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

EscritaO 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.cMostra 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:

inline int metade(int x) { return x / 2; }

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:

gcc -std=c23 -o - dobro.c

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 o call.
  • inline é uma dica. Com -O0, nada é expandido; com -O2, o compilador expande funções pequenas, com ou sem inline.
  • Em C, inline sozinho não cria a versão de verdade da função: com -O0, dá referência não definida no ligador. No C++ não é assim.
  • O jeito seguro é static inline, inclusive em arquivos .h. A alternativa do C99 é inline no .h + extern inline num único .c.
  • Prefira static inline a macros com parâmetros: o argumento é calculado uma vez, não há surpresa de precedência e os tipos são conferidos.