Por que as funções C não podem ser alteradas pelo nome?


136

Eu tive uma entrevista recentemente e uma pergunta foi: qual é o uso do extern "C"código C ++. Eu respondi que é para usar funções C no código C ++, pois C não usa nomes diferentes. Perguntaram-me por que C não usa nomes desconfiados e, para ser sincero, não consegui responder.

Entendo que, quando o compilador C ++ compila funções, ele atribui um nome especial à função, principalmente porque podemos ter funções sobrecarregadas com o mesmo nome em C ++, que devem ser resolvidas em tempo de compilação. Em C, o nome da função permanecerá o mesmo, ou talvez com um _ antes dele.

Minha pergunta é: o que há de errado em permitir que o compilador C ++ também modifique as funções C? Eu teria assumido que não importa que nomes o compilador lhes dê. Chamamos funções da mesma maneira em C e C ++.


75
C não precisa alterar os nomes, porque não possui sobrecarga de função.
— EOF

9
Como você vincula as bibliotecas C ao código C ++ se o compilador C ++ gerencia os nomes das funções?
— Mat

6
"Eu respondi que é para usar funções C no código C ++, pois C não usa nomes diferentes." - Eu acho que é o contrário. Externo "C" torna as funções C ++ utilizáveis ​​em um compilador C. fonte
— rozina

3
@ Engineer999: E se você compilar o subconjunto de C que também é C ++ com um compilador C ++, os nomes das funções serão realmente alterados. Mas se você deseja vincular binários criados com diferentes compiladores, não deseja desconfigurar nomes.
— EOF

13
C altera nomes. Normalmente, o nome desconfigurado é o nome da função precedida por um sublinhado. Às vezes, é o nome da função seguido por um sublinhado. extern "C"diz para alterar o nome da mesma maneira que "o" compilador C faria.
— Pete Becker

Respostas:


187

Foi meio que respondido acima, mas vou tentar colocar as coisas em contexto.

Primeiro, C veio primeiro. Como tal, o que C faz é, mais ou menos, o "padrão". Não altera nomes porque simplesmente não. Um nome de função é um nome de função. Um global é um global, e assim por diante.

Então C ++ apareceu. O C ++ queria poder usar o mesmo vinculador que C e vincular-se ao código escrito em C. Mas o C ++ não pôde deixar o C "confuso" (ou, falta dele) como está. Confira o seguinte exemplo:

int function(int a);
int function();

No C ++, essas são funções distintas, com corpos distintos. Se nenhum deles for mutilado, ambos serão chamados de "função" (ou "_função") e o vinculador reclamará da redefinição de um símbolo. A solução C ++ foi alterar os tipos de argumento para o nome da função. Então, um é chamado _function_inte o outro é chamado _function_void(não é o esquema de confinamento real) e a colisão é evitada.

Agora ficamos com um problema. Se int function(int a)foi definido em um módulo C, e estamos apenas pegando seu cabeçalho (ou seja, declaração) no código C ++ e usando-o, o compilador gerará uma instrução para o vinculador importar _function_int. Quando a função foi definida, no módulo C, não foi chamada assim. Foi chamado _function. Isso causará um erro no vinculador.

Para evitar esse erro, durante a declaração da função, dizemos ao compilador que é uma função projetada para ser vinculada ou compilada por um compilador C:

extern "C" int function(int a);

O compilador C ++ agora sabe importar _functione não _function_int, e está tudo bem.


1
@ ShacharShamesh: Eu perguntei isso em outro lugar, mas, e quanto a vincular em bibliotecas compiladas em C ++? Quando o compilador está percorrendo e compilando meu código que chama uma das funções em uma biblioteca compilada em C ++, como ele sabe qual nome deve ser modificado ou atribuído à função ao apenas ver sua declaração ou chamada de função? Como saber que, onde é definido, é mutilado por nome para outra coisa? Portanto, deve haver um método padrão de separação de nomes em C ++?
— Engineer999

2
Todo compilador faz isso da sua maneira especial. Se você estiver compilando tudo com o mesmo compilador, isso não importa. Mas se você tentar usar, digamos, uma biblioteca que foi compilada com o compilador da Borland, a partir de um programa que você está construindo com o compilador da Microsoft, bem ... boa sorte; você vai precisar dele :)
— Mark VY

6
@ Engineer999 Você já se perguntou por que não existem bibliotecas portáteis em C ++, mas elas especificam exatamente qual versão (e sinalizadores) do compilador (e biblioteca padrão) você deve usar ou apenas exportam uma API C? Ai está. C ++ é praticamente a linguagem menos portátil já inventada, enquanto C é exatamente o oposto. Há esforços nesse sentido, mas por agora, se você quer algo que é verdadeiramente portátil você vai ficar com C.
— Voo

1
@Voo Bem, em teoria, você deve escrever código portátil apenas aderindo ao padrão -std=c++11, por exemplo , e evitar o uso de qualquer coisa fora do padrão. É o mesmo que declarar uma versão Java (embora as versões mais recentes do Java sejam compatíveis com versões anteriores). Não é culpa dos padrões que as pessoas usam extensões específicas do compilador e código dependente da plataforma. Por outro lado, você não pode culpá-los, pois há muitas coisas (especialmente IO, como soquetes) ausentes no padrão. O comitê parece estar se aproximando lentamente disso. Corrija-me se eu perdi alguma coisa.
— Mucaho 14/04

14
@mucaho: você está falando sobre portabilidade / compatibilidade de fontes. ou seja, a API. Voo está falando sobre compatibilidade binária , sem uma recompilação. Isso requer compatibilidade com ABI . Compiladores C ++ alteram regularmente sua ABI entre versões. (por exemplo, o g ++ nem tenta ter uma ABI estável. Suponho que eles não quebram a ABI apenas por diversão, mas não evitam mudanças que exijam uma alteração da ABI quando há algo a ser ganho e não há outra maneira boa para fazê-lo.).
— Peter Cordes

45

Não é que eles "não possam", eles não sejam , em geral.

Se você deseja chamar uma função em uma biblioteca C chamada foo(int x, const char *y), não é bom permitir que o compilador C ++ faça isso foo_I_cCP()(ou seja o que for, apenas crie um esquema de manipulação aqui) apenas porque pode.

Esse nome não será resolvido, a função está em C e seu nome não depende da lista de tipos de argumentos. Portanto, o compilador C ++ precisa saber disso e marcar essa função como C para evitar a manipulação.

Lembre-se de que a função C pode estar em uma biblioteca cujo código-fonte você não possui, tudo o que você tem é o binário pré-compilado e o cabeçalho. Portanto, seu compilador C ++ não pode fazer "é coisa própria", afinal não pode mudar o que está na biblioteca.


Esta é a parte que estou perdendo. Por que o compilador C ++ alteraria um nome de função quando vê apenas sua declaração ou quando é chamada. Ele não apenas modifica os nomes das funções quando vê sua implementação? Isso faria mais sentido para mim
— Engineer999

13
@ Engineer999: Como você pode ter um nome para a definição e outro para a declaração? "Existe uma função chamada Brian que você pode chamar." "Ok, eu ligo para Brian." "Desculpe, não existe uma função chamada Brian." Acontece que se chama Graham.
— Lightness Races in Orbit

Que tal vincular em bibliotecas compiladas em C ++? Quando o compilador está percorrendo e compilando nosso código que chama uma das funções em uma biblioteca compilada em C ++, como ele sabe qual nome deve ser modificado ou atribuído à função ao apenas ver sua declaração ou chamada de função?
— Engineer999

1
@ Engineer999 Ambos devem concordar com o mesmo erro. Então eles veem o arquivo de cabeçalho (lembre-se, há muito poucos metadados nas DLLs nativas - os cabeçalhos são esses metadados) e dizem "Ah, certo, Brian deveria realmente ser Graham". Se isso não funcionar (por exemplo, com dois esquemas de manipulação incompatíveis), você não receberá um link correto e seu aplicativo falhará. O C ++ tem muitas incompatibilidades como essa. Na prática, você deve usar explicitamente o nome desconfigurado e desabilitar o desconforto do seu lado (por exemplo, você diz ao seu código para executar Graham, não Brian). Na prática real ... extern "C":)
— Luaan

1
@ Engineer999 Posso estar errado, mas você talvez tenha experiência com linguagens como Visual Basic, C # ou Java (ou mesmo Pascal / Delphi até certo ponto)? Aqueles fazem a interoperabilidade parecer extremamente simples. Em C e especialmente C ++, é tudo menos. Há muitas convenções de chamada que você precisa honrar, precisa saber quem é responsável por qual memória e você deve ter os arquivos de cabeçalho que informam as declarações de função, já que as próprias DLLs não contêm informações suficientes - especialmente no caso de C. puro. Se você não possui um arquivo de cabeçalho, geralmente precisa descompilar a DLL para usá-lo.
— Luaan

32

o que há de errado em permitir que o compilador C ++ também modifique as funções C?

Eles não seriam mais funções C.

Uma função não é apenas uma assinatura e uma definição; como uma função funciona é amplamente determinada por fatores como a convenção de chamada. A "Interface Binária do Aplicativo" especificada para uso em sua plataforma descreve como os sistemas se comunicam. A ABI do C ++ em uso pelo seu sistema especifica um esquema de manipulação de nomes, para que os programas nesse sistema saibam invocar funções nas bibliotecas e assim por diante. (Leia a ABI do C ++ Itanium para um ótimo exemplo. Você verá muito rapidamente por que é necessário.)

O mesmo se aplica à ABI C do seu sistema. Algumas ABIs C realmente têm um esquema de identificação de nomes (por exemplo, Visual Studio), portanto, trata-se menos de "desativar a identificação de nomes" e mais sobre como alternar da C ++ ABI para a C ABI, para determinadas funções. Marcamos funções C como sendo funções C, às quais o C ABI (em vez do C ++ ABI) é pertinente. A declaração deve corresponder à definição (seja no mesmo projeto ou em alguma biblioteca de terceiros), caso contrário, a declaração não faz sentido. Sem isso, seu sistema simplesmente não saberá como localizar / chamar essas funções.

Por que as plataformas não definem as ABIs C e C ++ iguais e se livram desse "problema", isso é parcialmente histórico - as AB AB originais não eram suficientes para C ++, que possui namespaces, classes e sobrecarga de operadores, todos dos quais precisam de alguma forma ser representados no nome de um símbolo de uma maneira amigável para o computador - mas também se pode argumentar que fazer com que os programas em C agora respeitem o C ++ seja injusto com a comunidade C, que teria que aturar uma complexidade muito mais complicada ABI apenas pelo interesse de outras pessoas que desejam interoperabilidade.


2
+int(PI/3), mas com um grão de sal: eu seria muito cauteloso ao falar de "C ++ ABI" ... AFAIK, existem tentativas de definir ABIs C ++, mas nenhum padrão real de fato / de jure - como isocpp.org/files /papers/n4028.pdf afirma (e eu concordo plenamente), citação, é profundamente irônico que o C ++ realmente sempre apoiou uma maneira de publicar uma API com uma ABI binária estável - recorrendo ao subconjunto C do C ++ via extern “C ”. . C++ Itanium ABIé apenas isso - alguns ++ ABI C para Itanium ... como discutido em stackoverflow.com/questions/7492180/c-abi-issues-list

3
@ vaxquis: Sim, não "ABI do C ++", mas "um ABI do C ++" da mesma maneira que eu tenho uma "chave da casa" que não funciona em todas as casas. Acho que poderia ser mais claro, embora eu tenha tentado deixar o mais claro possível, começando com a frase "A ABI do C ++ em uso pelo seu sistema " . Deixei o clarificador em frases posteriores por brevidade, mas vou aceitar uma edição que reduz a confusão aqui!
— Lightness Races in Orbit

1
Os AIUI C abi tendiam a ser propriedade de uma plataforma, enquanto as C ++ ABIs tendiam a ser propriedade de um compilador individual e, muitas vezes, até mesmo a propriedade de uma versão individual de um compilador. Portanto, se você deseja vincular os módulos criados com diferentes ferramentas de fornecedores, precisará usar um C abi para a interface.
— plugwash

A instrução "funções com nome desconfigurado não seriam mais funções C" é exagerada - é perfeitamente possível chamar funções com nome desconfigurado a partir da baunilha C simples se o nome desconfigurado for conhecido. O fato de o nome mudar não o torna menos aderente ao C ABI, ou seja, não o torna menos uma função C. Por outro lado, faz mais sentido - o código C ++ não poderia chamar uma função C sem declará-lo "C", porque faria diferença de nome ao tentar vincular-se ao receptor.
— Peter - Restabelece Monica

@ PeterA.Schneider: Sim, a frase do título é exagerada. O restante da resposta contém os detalhes factuais pertinentes.
— Lightness Races in Orbit

21

O MSVC de fato altera os nomes C, embora de maneira simples. Às vezes acrescenta @4ou outro número pequeno. Isso está relacionado às convenções de chamada e à necessidade de limpeza da pilha.

Portanto, a premissa é falha.


2
Na verdade, esse não é um nome desconcertante. É simplesmente uma convenção de nomenclatura específica do fornecedor (ou adornamento de nomes) para impedir que problemas com executáveis ​​sejam vinculados a DLLs criadas com as funções que possuem convenções de chamada diferentes.
— Peter

2
E quanto a anexar com a _?
— OrangeDog

12
@ Peter: Literalmente a mesma coisa.
— Lightness Races in Orbit

5
@Frankie_C: "O chamador limpa a pilha" não é especificado por nenhum padrão C: nenhuma convenção de chamada é mais padrão do que a outra na perspectiva do idioma.
— Ben Voigt

2
E da perspectiva da MSVC, a "convenção de chamada padrão" é exatamente o que você escolhe /Gd, /Gr, /Gv, /Gz. (Ou seja, a convenção de chamada padrão é usada, a menos que uma declaração de função especifique explicitamente uma convenção de chamada). Você está pensando em __cdeclqual é a convenção de chamada padrão padrão.
— precisa saber é o seguinte

13

É muito comum ter programas parcialmente escritos em C e parcialmente escritos em alguma outra linguagem (geralmente linguagem assembly, mas às vezes Pascal, FORTRAN ou outra coisa). Também é comum que os programas contenham componentes diferentes escritos por pessoas diferentes que talvez não tenham o código-fonte para tudo.

Na maioria das plataformas, existe uma especificação - geralmente chamada ABI [Application Binary Interface], que descreve o que um compilador deve fazer para produzir uma função com um nome específico que aceita argumentos de alguns tipos específicos e retorna um valor de algum tipo específico. Em alguns casos, uma ABI pode definir mais de uma "convenção de chamada"; Os compiladores para esses sistemas geralmente fornecem um meio de indicar qual convenção de chamada deve ser usada para uma função específica. Por exemplo, no Macintosh, a maioria das rotinas do Toolbox usa a convenção de chamada Pascal; portanto, o protótipo para algo como "LineTo" seria algo como:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

Se todo o código em um projeto foi compilado usando o mesmo compilador, não importaria qual nome o compilador exportou para cada função, mas em muitas situações será necessário que o código C chame funções que foram compiladas usando outras ferramentas e não pode ser recompilado com o compilador atual [e pode muito bem nem estar em C]. Ser capaz de definir o nome do vinculador é, portanto, fundamental para o uso de tais funções.


Sim, essa é a resposta. Se for apenas C e C ++, é difícil entender por que isso é feito dessa maneira. Para entender, devemos colocar as coisas no contexto da velha maneira de vincular estaticamente. A vinculação estática parece primitiva para os programadores do Windows, mas é o principal motivo pelo qual C não pode alterar nomes.
— user34660

2
@ user34660: Não existe. É a razão pela qual C não pode exigir a existência de recursos cuja implementação exigiria nomes exportáveis ​​confusos ou permitir a existência de vários símbolos com nomes semelhantes que se distinguem por características secundárias.
— Supercat

sabemos que houve tentativas de "ordenar" essas coisas ou que essas extensões eram extensões disponíveis para C antes de C ++?
— user34660

@ user34660: Re "O link estático parece primitivo para os programadores do Windows ...", mas o link dinâmico às vezes parece uma grande PITA para as pessoas que usam Linux, ao instalar o programa X (provavelmente escrito em C ++) significa ter que rastrear e instalar versões específicas de bibliotecas das quais você já possui versões diferentes no seu sistema.
— Jamesqf #

@jamesqf, sim, o Unix não tinha vínculo dinâmico antes do Windows. Eu sei muito pouco sobre links dinâmicos no Unix / Linux, mas parece que não é tão fácil quanto poderia ser em um sistema operacional em geral.
— user34660

12

Acrescentarei outra resposta, para abordar algumas das discussões tangenciais que ocorreram.

O C ABI (interface binária do aplicativo) originalmente pedia a passagem de argumentos na pilha na ordem inversa (ou seja, pressionada da direita para a esquerda), onde o chamador também libera o armazenamento da pilha. A ABI moderna realmente usa registradores para passar argumentos, mas muitas das considerações desconcertantes remontam à passagem original do argumento da pilha.

O Pascal ABI original, em contraste, empurrou os argumentos da esquerda para a direita, e o chamado teve que aparecer. O ABI C original é superior ao ABI Pascal original em dois pontos importantes. A ordem de envio de argumentos significa que o deslocamento da pilha do primeiro argumento é sempre conhecido, permitindo funções que possuem um número desconhecido de argumentos, onde os argumentos iniciais controlam quantos outros argumentos existem (ala printf).

A segunda maneira pela qual a C ABI é superior é o comportamento, caso o chamador e o destinatário não concordem com quantos argumentos existem. No caso C, desde que você não acesse argumentos anteriores ao último, nada de ruim acontece. Em Pascal, o número errado de argumentos é exibido na pilha e a pilha inteira está corrompida.

O ABI original do Windows 3.1 foi baseado em Pascal. Como tal, utilizou o Pascal ABI (argumentos da esquerda para a direita, chamados poplee). Como qualquer incompatibilidade no número do argumento pode levar à corrupção da pilha, um esquema confuso foi formado. Cada nome de função foi desconfigurado com um número indicando o tamanho, em bytes, de seus argumentos. Portanto, na máquina de 16 bits, a seguinte função (sintaxe C):

int function(int a)

Foi manipulado para function@2, porque inttem dois bytes de largura. Isso foi feito para que, se a declaração e a definição não coincidirem, o vinculador falhará ao encontrar a função em vez de corromper a pilha no tempo de execução. Por outro lado, se o programa for vinculado, você poderá ter certeza de que o número correto de bytes será exibido da pilha no final da chamada.

Windows de 32 bits e em diante use a stdcallABI. É semelhante ao Pascal ABI, exceto que a ordem de envio é como em C, da direita para a esquerda. Assim como a Pascal ABI, o nome mangling manipula o tamanho do byte de argumentos no nome da função para evitar a corrupção da pilha.

Diferentemente das declarações feitas em outros lugares aqui, o C ABI não altera os nomes das funções, mesmo no Visual Studio. Por outro lado, as funções de desconfiguração decoradas com a stdcallespecificação ABI não são exclusivas do VS. O GCC também suporta essa ABI, mesmo ao compilar para Linux. Isso é usado extensivamente pelo Wine , que usa seu próprio carregador para permitir a vinculação em tempo de execução dos binários compilados do Linux às DLLs compiladas do Windows.


9

Os compiladores C ++ usam nomes diferentes para permitir nomes de símbolos exclusivos para funções sobrecarregadas cuja assinatura seria a mesma. Ele também basicamente codifica os tipos de argumentos, o que permite o polimorfismo em um nível baseado em função.

C não exige isso, pois não permite a sobrecarga de funções.

Observe que o mangling de nome é um (mas certamente não é o único!) Motivo pelo qual não se pode confiar em uma 'C ++ ABI'.


8

O C ++ deseja poder interoperar com o código C vinculado a ele ou com o qual está vinculado.

C espera nomes de funções sem nome.

Se o C ++ o manipulasse, ele não encontraria as funções não-mutiladas exportadas de C ou C não encontraria as funções exportadas pelo C ++. O vinculador C deve obter o nome que ele espera, porque não sabe que é proveniente ou está indo para o C ++.


3

A manipulação incorreta dos nomes das funções e variáveis ​​C permitiria que seus tipos fossem verificados no momento do link. Atualmente, todas as implementações (?) C permitem definir uma variável em um arquivo e chamá-la como uma função em outro. Ou você pode declarar uma função com uma assinatura incorreta (por exemplo, void fopen(double)e depois chamá-la.

Propus um esquema para a ligação segura de tipo de variáveis ​​e funções C através do uso de manipulação em 1991. O esquema nunca foi adotado, porque, como outros já observaram aqui, isso destruiria a compatibilidade com versões anteriores.


1
Você quer dizer "permitir que seus tipos sejam verificados no momento do link ". Os tipos são verificados no momento da compilação, mas a vinculação com nomes não manipulados não pode verificar se as declarações usadas nas diferentes unidades de compilação estão de acordo. E se eles não concordarem, é o seu sistema de criação que está fundamentalmente quebrado e precisa ser corrigido.
— Cmaster - reinstate monica
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.