Por que os pointers de function e pointers de dados são incompatíveis em C / C ++?

Eu li que a conversão de um ponteiro de function para um ponteiro de dados e vice-versa funciona na maioria das plataformas, mas não é garantido que funcione. Por que esse é o caso? Os dois não devem ser simplesmente endereços na memory principal e, portanto, serem compatíveis?

Uma arquitetura não precisa armazenar código e dados na mesma memory. Com uma arquitetura Harvard, o código e os dados são armazenados em uma memory completamente diferente. A maioria das arquiteturas são arquiteturas Von Neumann com código e dados na mesma memory, mas C não se limita a apenas certos tipos de arquiteturas, se possível.

Alguns computadores possuem espaços de endereço separados para código e dados. Em tal hardware, simplesmente não funciona.

A linguagem é projetada não apenas para aplicativos de desktop atuais, mas para permitir que ela seja implementada em um grande conjunto de hardware.


Parece que o comitê de linguagem C nunca pretendeu que void* fosse um ponteiro para function, eles só queriam um ponteiro genérico para objects.

O Racional C99 diz:

6.3.2.3 Ponteiros
C agora foi implementado em uma ampla gama de arquiteturas. Enquanto algumas dessas arquiteturas apresentam pointers uniformes que são do tamanho de algum tipo inteiro, o código maximamente portátil não pode assumir qualquer correspondência necessária entre os diferentes tipos de ponteiro e os tipos inteiros. Em algumas implementações, os pointers podem até ser mais largos do que qualquer tipo inteiro.

O uso de void* (“pointer to void ”) como um tipo genérico de ponteiro de object é uma invenção do Comitê C89. A adoção desse tipo foi estimulada pelo desejo de especificar argumentos de protótipo de function que silenciosamente convertem pointers arbitrários (como em fread ) ou queixam-se se o tipo de argumento não corresponder exatamente (como em strcmp ). Nada é dito sobre pointers para funções, que podem ser incomensuráveis ​​com pointers de objects e / ou inteiros.

Nota Nada é dito sobre pointers para funções no último parágrafo. Eles podem ser diferentes de outros indicadores, e o comitê está ciente disso.

Para aqueles que se lembram do MS-DOS, do Windows 3.1 e mais antigos, a resposta é bem fácil. Todas elas são usadas para suportar vários modelos de memory diferentes, com diversas combinações de características para indicadores de código e dados.

Então, por exemplo, para o modelo Compact (código pequeno, dados grandes):

 sizeof(void *) > sizeof(void(*)()) 

e inversamente no modelo Médio (código grande, dados pequenos):

 sizeof(void *) < sizeof(void(*)()) 

Nesse caso, você não tinha armazenamento separado para código e data, mas ainda não conseguia converter entre os dois pointers (a não ser usando modificadores __near e __far não-padrão).

Além disso, não há garantia de que, mesmo se os pointers forem do mesmo tamanho, eles apontem para a mesma coisa - no modelo de memory pequena do DOS, tanto código quanto dados usados ​​próximos a pointers, mas eles apontaram para segmentos diferentes. Portanto, converter um ponteiro de function em um ponteiro de dados não forneceria um ponteiro que tivesse qualquer relação com a function e, portanto, não haveria uso para essa conversão.

Os pointers a serem invalidados devem ser capazes de acomodar um ponteiro para qualquer tipo de dado – mas não necessariamente um ponteiro para uma function. Alguns sistemas têm requisitos diferentes para pointers para funções do que pointers para dados (por exemplo, há DSPs com endereçamento diferente para dados versus código, modelo de mídia no MS-DOS usado pointers de 32 bits para código, mas somente pointers de 16 bits para dados) .

Além do que já foi dito aqui, é interessante ver o POSIX dlsym() :

O padrão ISO C não exige que os pointers para funções possam ser direcionados para os pointers dos dados. De fato, o padrão ISO C não exige que um object do tipo void * possa conter um ponteiro para uma function. As implementações que suportam a extensão XSI, no entanto, exigem que um object do tipo void * possa conter um ponteiro para uma function. O resultado da conversão de um ponteiro para uma function em um ponteiro para outro tipo de dados (exceto void *) ainda é indefinido, no entanto. Observe que os compiladores em conformidade com o padrão ISO C são necessários para gerar um aviso se uma conversão de um ponteiro void * para um ponteiro de function for tentada como em:

  fptr = (int (*)(int))dlsym(handle, "my_function"); 

Devido ao problema observado aqui, uma versão futura pode adicionar uma nova function para retornar os pointers de function, ou a interface atual pode ser preterida em favor de duas novas funções: uma que retorna pointers de dados e outra que retorna pointers de function.

C ++ 11 tem uma solução para a incompatibilidade de longa data entre C / C ++ e POSIX em relação a dlsym() . Pode-se usar reinterpret_cast para converter um ponteiro de function para / de um ponteiro de dados, desde que a implementação suporte esse recurso.

Do padrão, 5.2.10 para. 8, “converter um ponteiro de function em um tipo de ponteiro de object ou vice-versa é suportado condicionalmente.” 1.3.5 define “suportado condicionalmente” como uma “construção de programa que uma implementação não é necessária para suportar”.

Dependendo da arquitetura de destino, o código e os dados podem ser armazenados em áreas de memory fundamentalmente incompatíveis e fisicamente distintas.

indefinido não significa necessariamente não permitido, pode significar que o implementador do compilador tem mais liberdade para fazer como quiser.

Por exemplo, pode não ser possível em algumas arquiteturas – undefined permite que elas ainda tenham uma biblioteca ‘C’ em conformidade, mesmo que você não possa fazer isso.

Eles podem ser tipos diferentes com requisitos de espaço diferentes. Atribuir a um pode irreversivelmente dividir o valor do ponteiro para que a atribuição de resultados resulte em algo diferente.

Eu acredito que eles podem ser de tipos diferentes porque o padrão não quer limitar possíveis implementações que economizam espaço quando não são necessárias ou quando o tamanho pode fazer com que a CPU tenha que fazer mais porcarias para usá-lo, etc …

Outra solução

Supondo que POSIX garanta que os pointers de function e dados tenham o mesmo tamanho e representação (não consigo encontrar o texto para isso, mas o exemplo que o OP citou sugere que pelo menos pretendam fazer esse requisito), o seguinte deve funcionar:

 double (*cosine)(double); void *tmp; handle = dlopen("libm.so", RTLD_LAZY); tmp = dlsym(handle, "cos"); memcpy(&cosine, &tmp, sizeof cosine); 

Isso evita violar as regras de aliasing passando pela representação char [] , que tem permissão para aliasar todos os tipos.

Ainda outra abordagem:

 union { double (*fptr)(double); void *dptr; } u; u.dptr = dlsym(handle, "cos"); cosine = u.fptr; 

Mas eu recomendaria a abordagem memcpy se você quiser absolutamente 100% correto C.

A única solução verdadeiramente portátil é não usar o dlsym para funções e, em vez disso, usar o dlsym para obter um ponteiro para dados que contenham pointers de function. Por exemplo, na sua biblioteca:

 struct module foo_module = { .create = create_func, .destroy = destroy_func, .write = write_func, /* ... */ }; 

e depois na sua aplicação:

 struct module *foo = dlsym(handle, "foo_module"); foo->create(/*...*/); /* ... */ 

Incidentalmente, essa é uma boa prática de design e facilita o suporte a carregamento dynamic via dlopen e a vinculação estática de todos os módulos em sistemas que não suportam vinculação dinâmica ou onde o integrador de usuário / sistema não deseja usar vinculação dinâmica.

Na maioria das arquiteturas, os pointers para todos os tipos de dados normais têm a mesma representação, portanto, a conversão entre os tipos de ponteiro de dados é um não operacional.

No entanto, é concebível que os pointers de function possam exigir uma representação diferente, talvez sejam maiores do que outros pointers. Se void * pudesse conter pointers de function, isso significaria que a representação de void * teria que ser o tamanho maior. E todos os moldes de pointers de dados de / para void * teriam que executar essa cópia extra.

Como alguém mencionou, se você precisar disso, poderá alcançá-lo usando um sindicato. Mas a maioria dos usos de void * são apenas para dados, então seria oneroso aumentar todo o uso de memory deles apenas no caso de um ponteiro de function precisar ser armazenado.

Um exemplo moderno de onde os pointers de function podem diferir em tamanho dos pointers de dados: pointers de function de membro de class C ++

Diretamente citado em https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

 class Base1 { int b1; void Base1Method(); }; class Base2 { int b2; void Base2Method(); }; class Derived : public Base1, Base2 { int d; void DerivedMethod(); }; 

Existem agora dois possíveis this pointers.

Um ponteiro para uma function de membro de Base1 pode ser usado como um ponteiro para uma function de membro do Derived , uma vez que ambos usam o mesmo this ponteiro. Mas um ponteiro para uma function de membro de Base2 não pode ser usado como está como um ponteiro para uma function de membro de Derived , uma vez que o ponteiro this precisa ser ajustado.

Existem muitas maneiras de resolver isso. Veja como o compilador do Visual Studio decide lidar com isso:

Um ponteiro para uma function de membro de uma class herdada multiplicada é realmente uma estrutura.

 [Address of function] [Adjustor] 

O tamanho de uma function pointer-to-member de uma class que usa inheritance múltipla é o tamanho de um ponteiro mais o tamanho de um size_t .

tl; dr: Ao usar inheritance múltipla, um ponteiro para uma function-membro pode (dependendo do compilador, versão, arquitetura, etc.) ser armazenado como

 struct { void * func; size_t offset; } 

que é obviamente maior que um void * .

Eu sei que isso não foi comentado desde 2012, mas eu pensei que seria útil acrescentar que eu conheço uma arquitetura que tem pointers muito incompatíveis para dados e funções, já que uma chamada nessa arquitetura verifica o privilégio e carrega informações extras. Nenhuma quantidade de casting ajudará. É o moinho .