Esconder informações
Qual é a vantagem de retornar um ponteiro para uma estrutura em vez de retornar toda a estrutura na declaração de retorno da função?
O mais comum é a ocultação de informações . C não tem, digamos, a capacidade de tornar campos struct
particulares, e muito menos fornecer métodos para acessá-los.
Portanto, se você deseja impedir forçadamente os desenvolvedores de ver e mexer no conteúdo de um pontapé, por exemplo, FILE
a única maneira é impedir que eles sejam expostos à sua definição, tratando o ponteiro como opaco, cujo tamanho e ponta definição são desconhecidas para o mundo exterior. A definição de FILE
será visível apenas para aqueles que implementam as operações que requerem sua definição, como fopen
, enquanto apenas a declaração da estrutura será visível para o cabeçalho público.
Compatibilidade binária
Ocultar a definição da estrutura também pode ajudar a fornecer espaço para respirar para preservar a compatibilidade binária nas APIs do dylib. Ele permite que os implementadores da biblioteca alterem os campos na estrutura opaca sem quebrar a compatibilidade binária com aqueles que usam a biblioteca, uma vez que a natureza de seu código precisa apenas saber o que eles podem fazer com a estrutura, não o tamanho ou o tamanho dos campos. tem.
Como exemplo, eu posso realmente executar alguns programas antigos criados durante a era do Windows 95 hoje (nem sempre perfeitamente, mas surpreendentemente muitos ainda funcionam). Provavelmente, parte do código desses binários antigos usava ponteiros opacos para estruturas cujo tamanho e conteúdo foram alterados desde a era do Windows 95. No entanto, os programas continuam funcionando em novas versões do Windows, pois não foram expostos ao conteúdo dessas estruturas. Ao trabalhar em uma biblioteca onde a compatibilidade binária é importante, o que o cliente não está exposto geralmente pode mudar sem quebrar a compatibilidade com versões anteriores.
Eficiência
Retornar uma estrutura completa que é NULL seria mais difícil, suponho, ou menos eficiente. Este é um motivo válido?
Normalmente, é menos eficiente presumir que o tipo possa praticamente caber e ser alocado na pilha, a menos que exista um alocador de memória muito menos generalizado sendo usado nos bastidores do que malloc
, como uma memória de pool de alocadores de tamanho fixo e não variável já alocada. É uma troca de segurança nesse caso, provavelmente, permitir que os desenvolvedores da biblioteca mantenham invariantes (garantias conceituais) relacionados a FILE
.
Não é um motivo tão válido, pelo menos do ponto de vista do desempenho, para fazer fopen
retornar um ponteiro, pois o único motivo pelo qual ele retornaria NULL
é a falha ao abrir um arquivo. Isso seria otimizar um cenário excepcional em troca da lentidão de todos os caminhos de execução de casos comuns. Em alguns casos, pode haver um motivo válido de produtividade para tornar os projetos mais diretos e fazer com que retornem ponteiros para permitir o NULL
retorno em alguma pós-condição.
Para operações de arquivo, a sobrecarga é relativamente trivial em comparação com as próprias operações de arquivo, e o manual fclose
não pode ser evitado de qualquer maneira. Portanto, não é possível poupar ao cliente o aborrecimento de liberar (fechar) o recurso, expondo a definição FILE
e devolvendo-o por valor fopen
ou esperar muito aumento de desempenho, considerando o custo relativo das operações de arquivo para evitar uma alocação de heap .
Pontos de acesso e correções
Em outros casos, porém, eu criei um perfil de muitos códigos C desperdiçados em bases de código herdadas com pontos de acesso malloc
e falhas desnecessárias de cache obrigatórias como resultado do uso frequente dessa prática com ponteiros opacos e da alocação desnecessária de coisas desnecessárias na pilha, às vezes em grandes laços.
Uma prática alternativa que eu uso é expor as definições de estrutura, mesmo que o cliente não as adultere, usando um padrão de convenção de nomenclatura para comunicar que ninguém mais deve tocar nos campos:
struct Foo
{
/* priv_* indicates that you shouldn't tamper with these fields! */
int priv_internal_field;
int priv_other_one;
};
struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);
Se houver preocupações de compatibilidade binária no futuro, achei bom o suficiente reservar espaço supérfluo para propósitos futuros, como:
struct Foo
{
/* priv_* indicates that you shouldn't tamper with these fields! */
int priv_internal_field;
int priv_other_one;
/* reserved for possible future uses (emergency backup plan).
currently just set to null. */
void* priv_reserved;
};
Esse espaço reservado é um pouco inútil, mas pode salvar vidas se descobrirmos no futuro que precisamos adicionar mais alguns dados Foo
sem quebrar os binários que usam nossa biblioteca.
Na minha opinião, ocultação de informações e compatibilidade binária geralmente são a única razão decente para permitir apenas a alocação de estruturas de heap além de estruturas de comprimento variável (o que sempre exigiria isso, ou pelo menos seria um pouco estranho de usar caso contrário, se o cliente tivesse que alocar memória na pilha de maneira VLA para alocar o VLS). Mesmo grandes estruturas costumam ser mais baratas para retornar por valor, se isso significa que o software trabalha muito mais com a memória quente na pilha. E mesmo que não fosse mais barato retornar pelo valor na criação, alguém poderia simplesmente fazer o seguinte:
int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
foo_something(&foo);
foo_destroy(&foo);
}
... para inicializar Foo
da pilha sem a possibilidade de uma cópia supérflua. Ou o cliente ainda tem a liberdade de alocar Foo
na pilha, se desejar, por algum motivo.