Por que muitas funções que retornam estruturas em C, na verdade, retornam ponteiros para estruturas?


49

Qual é a vantagem de retornar um ponteiro para uma estrutura em vez de retornar toda a estrutura na returninstrução da função?

Eu estou falando sobre funções como fopene outras funções de baixo nível, mas provavelmente existem funções de nível superior que retornam ponteiros para estruturas também.

Acredito que isso seja mais uma escolha de design do que apenas uma questão de programação e estou curioso para saber mais sobre as vantagens e desvantagens dos dois métodos.

Uma das razões pelas quais pensei que seria uma vantagem em retornar um ponteiro para uma estrutura é poder saber com mais facilidade se a função falhou ao retornar o NULLponteiro.

Devolver uma estrutura completaNULL seria mais difícil, suponho, ou menos eficiente. Este é um motivo válido?


10
@ JohnR.Strohm Eu tentei e realmente funciona. Uma função pode retornar uma estrutura .... Então, qual é o motivo disso não ser feito?
yoyo_fun

28
A pré-padronização C não permitiu que as estruturas fossem copiadas ou passadas por valor. A biblioteca padrão C tem muitos itens desta época que não seriam escritos hoje, por exemplo, demorou até C11 para que a gets()função totalmente mal projetada fosse removida. Alguns programadores ainda têm aversão a copiar estruturas, velhos hábitos morrem com dificuldade.
18717

26
FILE*é efetivamente um identificador opaco. O código do usuário não deve se importar com a estrutura interna.
CodesInChaos 19/10

3
Retornar por referência é apenas um padrão razoável quando você tem uma coleta de lixo.
Idan Arye

7
@ JohnR.Strohm O "muito sênior" do seu perfil parece voltar antes de 1989 ;-) - quando o ANSI C permitiu o que a K&R C não fez: Copie estruturas em atribuições, passagem de parâmetros e valores de retorno. O livro original de K&R declarou, de fato, explicitamente (estou parafraseando): "você pode fazer exatamente duas coisas com uma estrutura, pegar seu endereço & e acessar um membro .".
Peter - Restabelece Monica

Respostas:


61

Existem várias razões práticas pelas quais funções como fopenretornar ponteiros para, em vez de instâncias de structtipos:

  1. Você deseja ocultar a representação do structtipo do usuário;
  2. Você está alocando um objeto dinamicamente;
  3. Você está se referindo a uma única instância de um objeto por meio de várias referências;

No caso de tipos como FILE *, é porque você não deseja expor detalhes da representação do tipo para o usuário - um FILE *objeto serve como um identificador opaco e você apenas passa esse identificador para várias rotinas de E / S (e embora FILEmuitas vezes seja implementado como um structtipo, não precisa ser).

Portanto, você pode expor um tipo incompleto struct em um cabeçalho em algum lugar:

typedef struct __some_internal_stream_implementation FILE;

Embora não seja possível declarar uma instância de um tipo incompleto, você pode declarar um ponteiro para ela. Portanto, posso criar um FILE *e atribuir a ele por meio de fopen, freopenetc., mas não posso manipular diretamente o objeto para o qual ele aponta.

Também é provável que a fopenfunção esteja alocando um FILEobjeto dinamicamente, usando mallocou similar. Nesse caso, faz sentido retornar um ponteiro.

Finalmente, é possível que você esteja armazenando algum tipo de estado em um structobjeto e precise disponibilizá-lo em vários locais diferentes. Se você retornasse instâncias do structtipo, essas instâncias seriam objetos separados na memória uns dos outros e, eventualmente, ficariam fora de sincronia. Ao retornar um ponteiro para um único objeto, todos estão se referindo ao mesmo objeto.


31
Uma vantagem particular de usar o ponteiro como um tipo opaco é que a própria estrutura pode mudar entre as versões da biblioteca e você não precisa recompilar os chamadores.
Barmar 19/10/19

6
@ Barmar: De fato, a ABI Stability é o grande ponto de venda de C, e não seria tão estável sem indicadores opacos.
Matthieu M.

37

Existem duas maneiras de "retornar uma estrutura". Você pode retornar uma cópia dos dados ou retornar uma referência (ponteiro) para eles. Geralmente, é preferível retornar (e distribuir em geral) um ponteiro, por alguns motivos.

Primeiro, copiar uma estrutura leva muito mais tempo da CPU do que copiar um ponteiro. Se isso é algo que seu código faz com frequência, pode causar uma diferença perceptível no desempenho.

Segundo, não importa quantas vezes você copie um ponteiro, ele ainda está apontando para a mesma estrutura na memória. Todas as modificações nele serão refletidas na mesma estrutura. Mas se você copiar a estrutura em si e fizer uma modificação, a alteração será exibida apenas nessa cópia . Qualquer código que contenha uma cópia diferente não verá a alteração. Às vezes, muito raramente, é isso que você deseja, mas na maioria das vezes não é, e pode causar erros se você errar.


54
A desvantagem de retornar pelo ponteiro: agora você precisa rastrear a propriedade desse objeto e, possivelmente, liberá-lo. Além disso, a indireção do ponteiro pode ser mais cara do que uma cópia rápida. Existem muitas variáveis ​​aqui, portanto, usar ponteiros não é universalmente melhor.
amon

17
Além disso, atualmente, os ponteiros são de 64 bits na maioria das plataformas de desktop e servidor. Eu já vi mais do que algumas estruturas na minha carreira que caberiam em 64 bits. Portanto, nem sempre é possível dizer que copiar um ponteiro custa menos do que copiar uma estrutura.
Solomon Slow

37
Essa é principalmente uma boa resposta, mas eu discordo da parte , às vezes, muito raramente, é isso que você deseja, mas na maioria das vezes não é - muito pelo contrário. O retorno de um ponteiro permite vários tipos de efeitos colaterais indesejados e várias maneiras desagradáveis ​​de errar a propriedade de um ponteiro. Nos casos em que o tempo da CPU não é tão importante, prefiro a variante de cópia; se essa é uma opção, é muito menos suscetível a erros.
Doc Brown

6
Note-se que isso realmente se aplica apenas a APIs externas. Para funções internas, todo compilador até marginalmente competente das últimas décadas reescreverá uma função que retorne uma estrutura grande para pegar um ponteiro como argumento adicional e construir o objeto diretamente nele. Os argumentos entre imutável e mutável já foram feitos com bastante frequência, mas acho que todos podemos concordar que a afirmação de que estruturas de dados imutáveis ​​quase nunca são o que você deseja não é verdadeira.
Voo

6
Você também pode mencionar paredes de incêndio de compilação como um profissional para indicadores. Em programas grandes com cabeçalhos amplamente compartilhados, tipos incompletos com funções impedem a necessidade de recompilar toda vez que um detalhe de implementação é alterado. O melhor comportamento de compilação é na verdade um efeito colateral do encapsulamento que é alcançado quando a interface e a implementação são separadas. Retornar (e passar, atribuir) por valor precisa das informações de implementação.
Peter - Restabelece Monica

12

Além de outras respostas, às vezes vale a pena retornar um valor pequeno struct . Por exemplo, pode-se retornar um par de dados e algum código de erro (ou êxito) relacionado a ele.

Por exemplo, fopenretorna apenas um dado (o aberto FILE*) e, em caso de erro, fornece o código de erro através da errnovariável pseudo-global. Mas talvez seja melhor retornar um structdos dois membros: o FILE*identificador e o código de erro (que seria definido se o identificador do arquivo for NULL). Por razões históricas, não é o caso (e os erros são relatados através do errnoglobal, que hoje é uma macro).

Observe que o idioma Go possui uma notação interessante para retornar dois (ou alguns) valores.

Observe também que no Linux / x86-64 as convenções ABI e de chamada (consulte a página x86-psABI ) especificam que um structdos dois membros escalares (por exemplo, um ponteiro e um número inteiro, ou dois ponteiros ou dois números inteiros) é retornado através de dois registros (e isso é muito eficiente e não passa pela memória).

Portanto, no novo código C, retornar um C pequeno structpode ser mais legível, mais fácil de encadear e mais eficiente.


Na verdade pequenas estruturas são embalados em rdx:rax. Assim, struct foo { int a,b; };é devolvido embalado em rax(por exemplo, com shift / ou) e deve ser descompactado com shift / mov. Aqui está um exemplo no Godbolt . Mas o x86 pode usar os baixos 32 bits de um registro de 64 bits para operações de 32 bits sem se preocupar com os altos, por isso é sempre muito ruim, mas definitivamente pior do que usar 2 registradores na maioria das vezes para estruturas de 2 membros.
Peter Cordes

Relacionado: bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int> retorna o booleano na metade superior rax, então você precisa de uma constante de máscara de 64 bits para testá-lo test. Ou você poderia usar bt. Mas é chato comparar o chamador e o destinatário com o uso dl, o que os compiladores devem fazer para funções "particulares". Também relacionado: libstdc ++ std::optional<T>não é trivialmente copiável, mesmo quando T é, portanto, sempre retorna por meio de ponteiro oculto: stackoverflow.com/questions/46544019/… . (libc ++ 's é trivialmente-copiável)
Peter Cordes

@PeterCordes: suas coisas relacionadas são C ++, não C
Basile Starynkevitch

Opa, certo. Bem, a mesma coisa se aplica exatamente a struct { int a; _Bool b; };em C, se o interlocutor queria testar o booleano, porque estruturas trivialmente-copyable C ++ usam a mesma ABI como C.
Peter Cordes

11
Exemplo clássicodiv_t div()
chux - Restabelecer Monica 28/03

6

Você está no caminho certo

Os dois motivos mencionados são válidos:

Uma das razões pelas quais pensei que seria uma vantagem em retornar um ponteiro para uma estrutura é poder saber com mais facilidade se a função falhou ao retornar o ponteiro NULL.

Retornar uma estrutura FULL que é NULL seria mais difícil, suponho, ou menos eficiente. Este é um motivo válido?

Se você tem uma textura (por exemplo) em algum lugar da memória e deseja fazer referência a essa textura em vários locais do seu programa; não seria prudente fazer uma cópia sempre que você quisesse fazer referência a ela. Em vez disso, se você simplesmente passar um ponteiro para fazer referência à textura, seu programa será executado muito mais rápido.

O maior motivo é a alocação dinâmica de memória. Muitas vezes, quando um programa é compilado, você não tem certeza da quantidade exata de memória necessária para determinadas estruturas de dados. Quando isso acontece, a quantidade de memória que você precisa usar será determinada em tempo de execução. Você pode solicitar memória usando 'malloc' e liberá-lo quando terminar de usar 'free'.

Um bom exemplo disso é a leitura de um arquivo especificado pelo usuário. Nesse caso, você não tem idéia do tamanho do arquivo ao compilar o programa. Você só pode descobrir quanta memória precisa quando o programa está realmente sendo executado.

Malloc e ponteiros de retorno gratuitos para locais na memória. Portanto, as funções que fazem uso da alocação dinâmica de memória retornam os ponteiros para onde eles criaram suas estruturas na memória.

Além disso, nos comentários, vejo que há uma dúvida sobre se você pode retornar uma estrutura de uma função. Você pode realmente fazer isso. O seguinte deve funcionar:

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}

Como é possível não saber quanta memória uma determinada variável precisará se você já tiver o tipo de estrutura definido?
yoyo_fun

9
@JenniferAnderson C tem um conceito de tipos incompletos: um nome de tipo pode ser declarado mas ainda não definido, portanto, seu tamanho não está disponível. Não posso declarar variáveis ​​desse tipo, mas posso declarar ponteiros para esse tipo, por exemplo struct incomplete* foo(void). Dessa forma, posso declarar funções em um cabeçalho, mas apenas definir as estruturas dentro de um arquivo C, permitindo assim o encapsulamento.
amon

@ amon Então é assim que declarar cabeçalhos de funções (protótipos / assinaturas) antes de declarar como eles funcionam é realmente feito em C? E é possível fazer a mesma coisa para as estruturas e uniões em C
yoyo_fun

@JenniferAnderson, você declara protótipos de função (funções sem corpos) nos arquivos de cabeçalho e pode chamar essas funções em outro código, sem conhecer o corpo das funções, porque o compilador só precisa saber como organizar os argumentos e como aceitar o valor de retorno. No momento em que você vincula o programa, você precisa conhecer a definição da função (ou seja, com um corpo), mas você só precisa processá-la uma vez. Se você usar um tipo não simples, ele também precisará conhecer a estrutura desse tipo, mas os ponteiros geralmente têm o mesmo tamanho e isso não importa para o uso de um protótipo.
simpleuser 19/10/17

6

Algo como a FILE*não é realmente um ponteiro para uma estrutura no que diz respeito ao código do cliente, mas é uma forma de identificador opaco associado a alguma outra entidade, como um arquivo. Quando um programa é chamado fopen, geralmente ele não se importa com o conteúdo da estrutura retornada - tudo o que importa é que outras funções freadfaçam o que for necessário.

Se uma biblioteca padrão mantém dentro de uma FILE*informação sobre, por exemplo, a posição atual de leitura dentro desse arquivo, uma chamada para freadprecisa atualizar essa informação. Ter freadrecebido um ponteiro para o FILEfacilita isso. Se, em freadvez disso, recebesse um FILE, não haveria como atualizar o FILEobjeto mantido pelo chamador.


3

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 structparticulares, 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, FILEa ú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 FILEserá 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 fopenretornar 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 NULLretorno 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 fclosenã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 FILEe devolvendo-o por valor fopenou 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 malloce 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 Foosem 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 Fooda pilha sem a possibilidade de uma cópia supérflua. Ou o cliente ainda tem a liberdade de alocar Foona pilha, se desejar, por algum motivo.

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.