O “inline” sem “estático” ou “externo” é sempre útil no C99?

96

Quando tento construir este código

inline void f() {}

int main()
{
    f();
}

usando a linha de comando

gcc -std=c99 -o a a.c

Recebo um erro do vinculador (referência indefinida para f). O erro desaparece se eu usar static inlineou em extern inlinevez de apenas inline, ou se eu compilar com -O(portanto, a função é realmente embutida).

Este comportamento parece estar definido no parágrafo 6.7.4 (6) da norma C99:

Se todas as declarações de escopo de arquivo para uma função em uma unidade de tradução incluem o inlineespecificador de função sem extern, então a definição nessa unidade de tradução é uma definição embutida. Uma definição embutida não fornece uma definição externa para a função e não proíbe uma definição externa em outra unidade de tradução. Uma definição embutida fornece uma alternativa a uma definição externa, que um tradutor pode usar para implementar qualquer chamada à função na mesma unidade de tradução. Não é especificado se uma chamada para a função usa a definição embutida ou a definição externa.

Se bem entendi tudo isso, uma unidade de compilação com uma função definida inline como no exemplo acima só compila de forma consistente se houver também uma função externa com o mesmo nome, e nunca sei se minha própria função ou a função externa é chamada.

Este comportamento não é completamente idiota? É útil definir uma função inlinesem staticou externem C99? Estou esquecendo de algo?

Resumo das respostas

Claro que estava faltando alguma coisa, e o comportamento não é idiota. :)

Como explica Nemo , a ideia é colocar a definição da função

inline void f() {}

no arquivo de cabeçalho e apenas uma declaração

extern inline void f();

no arquivo .c correspondente. Apenas a externdeclaração aciona a geração de código binário visível externamente. E de fato não há uso deinline em um arquivo .c - só é útil em cabeçalhos.

Como explica o raciocínio do comitê C99 citado na resposta de Jonathan , inlinetudo gira em torno de otimizações do compilador que requerem a definição de uma função para ser visível no local de uma chamada. Isso só pode ser alcançado colocando a definição no cabeçalho e, claro, uma definição em um cabeçalho não deve emitir código toda vez que for vista pelo compilador. Mas, como o compilador não é forçado a embutir uma função, uma definição externa deve existir em algum lugar.

Sven Marnach
fonte
borderline duplicate of stackoverflow.com/questions/5369888/…
Earlz
@Earlz: Obrigado pelo link. Minha pergunta é sobre a lógica por trás do parágrafo citado do padrão, e se há casos de uso para inlinesem statice extern, no entanto. Infelizmente, nenhum desses problemas é abordado nessa questão.
Sven Marnach de
@Nemo: Eu li essa pergunta e as respostas antes de postar a minha. Novamente, não estou perguntando "como isso se comporta?" mas sim "qual é a ideia por trás desse comportamento"? Tenho certeza de que estou perdendo algo aqui.
Sven Marnach de
1
sim, é por isso que eu disse "limítrofe". Eu pessoalmente acho que isso é uma pergunta completamente diferente
Earlz de

Respostas:

40

Na verdade, esta excelente resposta também responde à sua pergunta, eu acho:

O que extern inline faz?

A ideia é que "embutido" pode ser usado em um arquivo de cabeçalho e, em seguida, "embutido externo" em um arquivo .c. "extern inline" é apenas como você instrui o compilador qual arquivo-objeto deve conter o código gerado (visível externamente).

[atualizar, para elaborar]

Eu não acho que haja qualquer uso para "inline" (sem "estático" ou "externo") em um arquivo .c. Mas, em um arquivo de cabeçalho, faz sentido e requer uma declaração "extern inline" correspondente em algum arquivo .c para realmente gerar o código independente.

Nemo
fonte
1
Obrigado, estou começando a ter uma ideia!
Sven Marnach de
8
Sim, eu mesmo nunca entendi isso até agora, então obrigado por perguntar :-). É estranho que a definição "embutida" não externa vá no cabeçalho (mas não necessariamente resulta em qualquer geração de código), enquanto a declaração "embutida externa" vai no arquivo .c e realmente faz com que o código ser gerado.
Nemo
3
Isso significa que escrevo inline void f() {}no cabeçalho e extern inline void f();no arquivo .c? Portanto, a definição real da função vai no cabeçalho e o arquivo .c contém uma mera declaração neste caso, no reverso da ordem usual?
Sven Marnach de
1
@endolith: Não entendi sua pergunta. Estamos discutindo inline sem static ou extern. Claro que static inlineestá bem, mas não é disso que se trata esta pergunta e resposta.
Nemo
2
@MatthieuMoy Você precisa usar em -std=c99vez de -std=gnu89.
A3f
27

Do próprio padrão (ISO / IEC 9899: 1999):

Apêndice J.2 Comportamento Indefinido

  • ...
  • Uma função com ligação externa é declarada com um inlineespecificador de função, mas também não é definida na mesma unidade de tradução (6.7.4).
  • ...

O Comitê C99 escreveu uma justificativa e diz:

6.7.4 Especificadores de função

Um novo recurso do C99: a inlinepalavra-chave, adaptada do C ++, é um especificador de função que pode ser usado apenas em declarações de função. É útil para otimizações de programa que requerem a definição de uma função para ficar visível no local de uma chamada. (Observe que o Padrão não tenta especificar a natureza dessas otimizações.)

A visibilidade é garantida se a função tiver vinculação interna, ou se houver vinculação externa e a chamada estiver na mesma unidade de tradução da definição externa. Nesses casos, a presença da inlinepalavra - chave em uma declaração ou definição da função não tem efeito além de indicar uma preferência de que as chamadas dessa função devem ser otimizadas em preferência às chamadas de outras funções declaradas sem oinline palavra chave.

A visibilidade é um problema para uma chamada de uma função com vínculo externo, onde a chamada está em uma unidade de tradução diferente da definição da função. Neste caso, oinline palavra chave permite que a unidade de tradução que contém a chamada também contenha uma definição local ou embutida da função.

Um programa pode conter uma unidade de tradução com uma definição externa, uma unidade de tradução com uma definição embutida e uma unidade de tradução com uma declaração, mas sem definição para uma função. As chamadas na última unidade de tradução usarão a definição externa normalmente.

Uma definição embutida de uma função é considerada uma definição diferente da definição externa. Se uma chamada para alguma função funccom vínculo externo ocorre onde uma definição inline é visível, o comportamento é o mesmo como se a chamada fosse feita para outra função, digamos __func , com vínculo interno. Um programa em conformidade não deve depender de qual função é chamada. Este é o modelo embutido no padrão.

Um programa em conformidade não deve depender da implementação usando a definição embutida, nem pode confiar na implementação usando a definição externa. O endereço de uma função é sempre o endereço correspondente à definição externa, mas quando esse endereço é usado para chamar a função, a definição embutida pode ser usada. Portanto, o exemplo a seguir pode não se comportar conforme o esperado.

inline const char *saddr(void)
{
    static const char name[] = "saddr";
    return name;
}
int compare_name(void)
{
    return saddr() == saddr(); // unspecified behavior
}

Visto que a implementação pode usar a definição embutida para uma das chamadas saddre usar a definição externa para a outra, a operação de igualdade não tem garantia de avaliação para 1 (verdadeiro). Isso mostra que os objetos estáticos definidos na definição embutida são distintos de seus objetos correspondentes na definição externa. Isso motivou a restrição até mesmo contra a definição de umaconst objeto desse tipo.

Inlining foi adicionado ao padrão de forma que possa ser implementado com a tecnologia de vinculação existente, e um subconjunto de inlining C99 é compatível com C ++. Isso foi conseguido exigindo que exatamente uma unidade de tradução contendo a definição de uma função inline seja especificada como aquela que fornece a definição externa para a função. Como essa especificação consiste simplesmente em uma declaração que não inlinecontém a palavra - chave ou contém as duas palavras inlinee extern, ela também será aceita por um tradutor C ++.

Inlining no C99 estende a especificação C ++ de duas maneiras. Primeiro, se uma função for declarada inlineem uma unidade de tradução, não precisa ser declarada inlineem todas as outras unidades de tradução. Isso permite, por exemplo, uma função de biblioteca que deve ser embutida na biblioteca, mas disponível apenas por meio de uma definição externa em outro lugar. A alternativa de usar uma função de wrapper para a função externa requer um nome adicional; e também pode afetar negativamente o desempenho se um tradutor não fizer a substituição inline.

Em segundo lugar, o requisito de que todas as definições de uma função inline sejam "exatamente iguais" é substituído pelo requisito de que o comportamento do programa não deve depender se uma chamada é implementada com uma definição inline visível, ou a definição externa, de um função. Isso permite que uma definição inline seja especializada para seu uso em uma unidade de tradução específica. Por exemplo, a definição externa de uma função de biblioteca pode incluir alguma validação de argumento que não é necessária para chamadas feitas de outras funções na mesma biblioteca. Essas extensões oferecem algumas vantagens; e os programadores preocupados com a compatibilidade podem simplesmente seguir as regras mais rígidas do C ++.

Note que é não apropriar para implementações de fornecer definições de inline de funções da biblioteca padrão nos cabeçalhos padrão, porque isso pode quebrar algum código legado que redeclares funções da biblioteca padrão após incluindo seus cabeçalhos. A inlinepalavra-chave destina-se apenas a fornecer aos usuários uma maneira portátil de sugerir funções embutidas. Como os cabeçalhos padrão não precisam ser portáteis, as implementações têm outras opções ao longo das linhas de:

#define abs(x) __builtin_abs(x)

ou outros mecanismos não portáteis para funções de biblioteca padrão embutidas.

Jonathan Leffler
fonte
Obrigado, isso é muito detalhado - e quase tão legível quanto o próprio padrão. :-) Eu sabia que devia estar faltando alguma coisa. Você poderia dar uma referência de onde você tirou isso?
Sven Marnach de
Acabei de passar pela minha cabeça que você pode usar o Google para encontrar links: open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf
Sven Marnach de
@Sven: Peguei emprestado o URL da sua pesquisa e coloquei na resposta. O documento que usei era uma cópia do raciocínio que salvei anteriormente (em 2005), mas também era V5.10.
Jonathan Leffler
4
Obrigado novamente. O insight mais valioso que obtive com a resposta é que existe um documento que contém a lógica do padrão C99 (e provavelmente também existem documentos para outros padrões). Embora a resposta de Nemo tenha me dado um peixe, este me ensinou a pescar.
Sven Marnach de
0

> Recebo um erro de vinculador (referência indefinida para f)

Funciona aqui: Linux x86-64, GCC 4.1.2. Pode ser um bug em seu compilador; Não vejo nada no parágrafo citado da norma que proíba o programa em questão. Observe o uso de if em vez de iff .

Uma definição embutida fornece uma alternativa a uma definição externa , que um tradutor pode usar para implementar qualquer chamada à função na mesma unidade de tradução.

Portanto, se você conhece o comportamento da função fe deseja chamá-la em um loop fechado, pode copiar e colar sua definição em um módulo para evitar chamadas de função; ou , você pode fornecer uma definição que, para os propósitos do módulo atual, seja equivalente (mas pula a validação de entrada ou qualquer otimização que você possa imaginar). O gravador do compilador, entretanto, tem a opção de otimizar o tamanho do programa.

Fred Foo
fonte
2
O padrão afirma que o compilador pode sempre presumir que existe uma função externa com o mesmo nome e chamá-la em vez da versão embutida, então não acho que seja um bug do compilador. Tentei com gcc 4.3, 4.4 e 4.5, todos dão um erro de vinculador.
Sven Marnach de