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

Qual é a vantagem de retornar um ponteiro para uma estrutura em vez de retornar a estrutura inteira no return declaração da função?

Estou falando sobre funções como fopen e outras funções de nível inferior, mas provavelmente há 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.

Um dos Os motivos pelos quais pensei que seria uma vantagem retornar um ponteiro para uma estrutura é poder saber mais facilmente se a função falhou, retornando NULL ponteiro.

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

Comentários

  • @ JohnR.Strohm Eu tentei e realmente funciona. Uma função pode retornar uma estrutura …. Então qual é o motivo de não ser feito?
  • A pré-padronização C não permitia que estruturas fossem copiadas ou passadas por valor. A biblioteca padrão C tem muitos resquícios daquela época que não seriam escritos dessa forma hoje, por exemplo levou até C11 para que a função gets() totalmente mal projetada fosse removida. Alguns programadores ainda têm aversão a copiar structs, velhos hábitos são difíceis de morrer.
  • FILE* é efetivamente um identificador opaco. O código do usuário não deve se importar com sua estrutura interna.
  • Retornar por referência é apenas um padrão razoável quando você tem a coleta de lixo.
  • @ JohnR.Strohm O ” muito sênior ” em seu perfil parece ser anterior a 1989 😉 – quando ANSI C permitia o que K & RC didn ‘ t: Copia estruturas em atribuições, passagem de parâmetros e valores de retorno. K & R ‘ s livro original de fato declarado explicitamente (eu ‘ m parafraseando): ” você pode fazer exatamente duas coisas com uma estrutura, pegar seu endereço com & e acessar um membro com .. ”

Resposta

Lá há vários motivos práticos pelos quais funções como fopen retornam ponteiros para em vez de instâncias de struct tipos:

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

No caso de tipos como FILE *, é porque você não deseja expor detalhes da representação do tipo para o usuário – um FILE * obje ct serve como um identificador opaco e você apenas passa esse identificador para várias rotinas de E / S (e enquanto FILE é frequentemente implementado como um struct tipo, 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 você não possa declarar uma instância de um tipo incompleto, você pode declarar um ponteiro para ele. Portanto, posso criar um FILE * e atribuir a ele por meio de fopen, freopen, etc. , mas não posso manipular diretamente o objeto para o qual ele aponta.

Também é provável que a função fopen esteja alocando um FILE objeto dinamicamente, usando malloc ou similar. Nesse caso, faz sentido retornar um ponteiro.

Finalmente, é possível que você esteja armazenando algum tipo de estado em um objeto struct, e você precisa disponibilizar esse estado em vários lugares diferentes. Se você retornasse instâncias do tipo struct, essas instâncias seriam objetos separados na memória umas das outras e, eventualmente, ficariam fora de sincronia. Ao retornar um ponteiro para um único objeto, todos estão se referindo ao mesmo objeto.

Comentários

  • 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 ‘ não precisa recompilar os chamadores.
  • @Barmar: Na verdade, a estabilidade ABI é o grande ponto de venda do C, e não seria tão estável sem ponteiros opacos.

Resposta

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

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

Segundo, não importa quantas vezes você copie um ponteiro, ele ainda estará apontando para a mesma estrutura na memória. Todas as modificações serão refletidas na mesma estrutura. Mas se você copiar a própria estrutura e, em seguida, fizer uma modificação, a mudança só aparecerá naquela 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 bugs se você errar.

Comentários

  • A desvantagem de retornar por ponteiro: agora você ‘ tem que rastrear a propriedade desse objeto e possível liberte-o. Além disso, a indireção do ponteiro pode ser mais cara do que uma cópia rápida. Existem muitas variáveis aqui, então usar ponteiros não é universalmente melhor.
  • Além disso, ponteiros hoje em dia são de 64 bits na maioria das plataformas de desktop e servidor. Eu ‘ vi mais do que algumas estruturas em minha carreira que caberiam em 64 bits. Portanto, você pode ‘ t sempre dizer que copiar um ponteiro custa menos do que copiar uma estrutura.
  • Esta é geralmente uma boa resposta , mas discordo sobre a parte às vezes, muito raramente, é isso que você deseja, mas na maioria das vezes ‘ não é – muito pelo contrário. Retornar um ponteiro permite vários tipos de efeitos colaterais indesejados e vários tipos de maneiras desagradáveis de obter a propriedade de um ponteiro errado. Nos casos em que o tempo de CPU não é tão importante, eu prefiro a variante de cópia, se for uma opção, é muito menos sujeita a erros.
  • Deve-se notar que isso realmente aplica-se apenas a APIs externas. Para funções internas, todo compilador mesmo marginalmente competente das últimas décadas reescreverá uma função que retorna uma grande estrutura para tomar um ponteiro como um argumento adicional e construir o objeto diretamente ali. Os argumentos de imutável versus mutável já foram usados com 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.
  • Você também pode mencionar firewalls de compilação como profissional de dicas. Em programas grandes com cabeçalhos amplamente compartilhados, tipos incompletos com funções evitam a necessidade de recompilar toda vez que um detalhe de implementação muda. O melhor comportamento de compilação é, na verdade, um efeito colateral do encapsulamento que é obtido quando a interface e a implementação são separadas. Retornar (e passar, atribuir) por valor precisa das informações de implementação.

Resposta

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

Para dar um exemplo, fopen retorna apenas um dado (o FILE* aberto) e, em caso de erro, fornece o código de erro por meio de errno variável pseudo-global. Mas talvez seja melhor retornar um struct de dois membros: o FILE* identificador e o código de erro (que seria definido se o identificador de arquivo é NULL). Por motivos históricos, não é o caso (e os erros são relatados por meio do errno global, que hoje é uma macro).

Observe que A linguagem Go tem uma boa notação para retornar dois (ou alguns) valores.

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

Portanto, no novo código C, retornar um pequeno C struct pode ser mais legível, mais amigável ao thread e mais eficiente.

Comentários

  • Na verdade, pequenas estruturas são compactadas em rdx:rax. Portanto, struct foo { int a,b; }; é retornado compactado em rax (por exemplo, com shift / ou) e deve ser descompactado com shift / mov. Aqui ‘ está um exemplo sobre Godbolt . Mas x86 pode usar os 32 bits baixos de um registro de 64 bits para operações de 32 bits sem se preocupar com os bits altos, então ‘ é sempre muito ruim, mas definitivamente pior do que usar 2 registra-se na maioria das vezes para estruturas de 2 membros.
  • Relacionado: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> retorna o booleano na metade superior de rax, então você precisa de uma máscara de 64 bits constante para testá-lo com test. Ou você pode usar bt. Mas é péssimo para o chamador e para o receptor comparar com o uso de dl, o que os compiladores deveriam fazer para ” private ” funções. Também relacionado: libstdc ++ ‘ s std::optional<T> isn ‘ t trivialmente copiável mesmo quando T é , então ele sempre retorna por meio do ponteiro oculto: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s é trivialmente copiável)
  • @PeterCordes: seus itens relacionados são C ++, não C
  • Oops, certo. Bem, a mesma coisa se aplicaria exatamente a struct { int a; _Bool b; }; em C, se o chamador quisesse testar o booleano, porque estruturas C ++ trivialmente copiáveis usam a mesma ABI que C.
  • Exemplo clássico div_t div()

Resposta

Você está no caminho certo

Os dois motivos mencionados são válidos:

Um dos motivos que eu pensei que seria uma vantagem retornar um ponteiro para uma estrutura é ser capaz de dizer mais facilmente se a função falhou ao retornar um ponteiro NULL.

Retornar uma estrutura FULL que é NULL seria mais difícil, suponho ou menos eficiente. Esta é uma razão válida?

Se você tem uma textura (por exemplo) em algum lugar na memória, e deseja fazer referência a essa textura em vários lugares em seu programa; não seria sensato fazer uma cópia toda vez que você quisesse referenciá-la. Em vez disso, se você simplesmente passar um ponteiro para referenciar a textura, seu programa será executado muito mais rápido.

Porém, o maior motivo é a alocação de memória dinâmica. Muitas vezes, quando um programa é compilado, você não tem certeza de quanta memória precisa para certas estruturas de dados. Quando isso acontece, a quantidade de memória que você precisa usar será determinada em tempo de execução. Você pode solicite memória usando malloc e depois libere-a quando terminar de usar free.

Um bom exemplo disso é a leitura de um arquivo especificado pelo usuário. Neste caso, você não tem idéia de quão grande o arquivo pode ser quando você compila o programa. Você só pode descobrir quanta memória você precisa quando o programa está realmente em execução.

Ambos malloc e ponteiros de retorno livre para locais na memória. que fazem uso de alocação de memória dinâmica retornarão ponteiros para onde criaram suas estruturas na memória.

Além disso, nos comentários, vejo que há uma dúvida se você pode retornar uma estrutura de uma função. Você realmente pode 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; } 

Comentários

  • Como é possível não saber quanta memória uma determinada variável será necessária se você já tiver o tipo de estrutura definido?
  • @JenniferAnderson C tem um conceito de tipos incompletos: um nome de tipo pode ser declarado, mas ainda não definido, então ‘ o 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 Portanto, é assim que declarar cabeçalhos de função (protótipos / assinaturas) antes de declarar como eles o trabalho é realmente feito em C? E é possível fazer o mesmo com as estruturas e uniões em C
  • @JenniferAnderson você declara a função protótipos (funções sem corpos) em arquivos de cabeçalho e pode então chamar essas funções em outro código, sem conhecer o corpo das funções, pois o compilador só precisa saber como organizar os argumentos e aceitar o valor de retorno. No momento em que vincula o programa, você realmente precisa saber a definição da função (ou seja, com um corpo), mas só precisa processá-la uma vez. Se você usar um tipo não simples, ele também precisa conhecer a estrutura do tipo ‘ s, mas os ponteiros geralmente têm o mesmo tamanho e não ‘ t importa para um protótipo ‘ s uso.

Resposta

Algo como 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 algum outra entidade como um arquivo. Quando um programa chama fopen, ele geralmente não se preocupa com o conteúdo da estrutura retornada – tudo o que importa é que outras funções como fread farão tudo o que for necessário com ele.

Se uma biblioteca padrão mantém dentro de FILE* informações sobre, por exemplo, a posição de leitura atual dentro desse arquivo, uma chamada para fread precisaria ser capaz de atualizar essas informações. Ter fread receber um ponteiro para FILE torna isso mais fácil. Se fread em vez disso recebesse um FILE, não haveria como atualizar o objeto FILE retida pelo chamador.

Resposta

Ocultação de informações

Qual é a vantagem de retornar um ponteiro para uma estrutura em oposição a retornar toda a estrutura na instrução de retorno de a função?

A mais comum é ocultação de informações . C não tem, digamos, a capacidade de tornar os campos de um struct privados, muito menos fornecer métodos para acessá-los.

Portanto, se você quiser forçar impedir que os desenvolvedores sejam capazes de ver e adulterar o conteúdo de um ponteiro, como FILE, então a única maneira é evitar que eles sejam expostos à sua definição tratando o ponteiro como opaco, cujo tamanho da ponta e definição são desconhecidos para o mundo exterior. A definição de FILE será então 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 dylib. Isso permite que os implementadores da biblioteca alterem os campos na estrutura opaca urei sem quebrar a compatibilidade binária com aqueles que usam a biblioteca, uma vez que a natureza de seu código só precisa saber o que eles podem fazer com a estrutura, não quão grande ela é ou quais campos ela possui.

Como um Por exemplo, eu posso realmente executar alguns programas antigos construídos durante a era Windows 95 hoje (nem sempre perfeitamente, mas surpreendentemente muitos ainda funcionam). É provável que parte do código desses binários antigos usasse ponteiros opacos para estruturas cujo tamanho e conteúdo mudaram desde a era do Windows 95. No entanto, os programas continuam a funcionar em novas versões do Windows, uma vez que 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 compatibilidade com versões anteriores.

Eficiência

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

É normalmente menos eficiente assumindo que o tipo pode caber praticamente e ser alocado na pilha, a menos que haja muito menos alocador de memória generalizado sendo usado em segundo plano do que malloc, como um alocador de tamanho fixo em vez de tamanho variável que agrupa a memória já alocada. É uma troca de segurança neste caso, a maioria provavelmente, para permitir que os desenvolvedores da biblioteca mantenham invariáveis (garantias conceituais) relacionadas a FILE.

Não é uma razão válida, pelo menos do ponto de vista de desempenho para fazer com que fopen retorne um ponteiro, já que a única razão para “d retornar NULL é a falha ao abrir um arquivo. Isso seria otimizar um cenário excepcional em troca de desacelerar todos os caminhos de execução de casos comuns. Pode haver uma razão de produtividade válida em alguns casos para tornar os designs mais simples para fazê-los retornar ponteiros para permitir que NULL seja retornado em alguma pós-condição.

Para operações de arquivo, a sobrecarga é relativamente bastante trivial em comparação com as operações de arquivo em si, e a necessidade manual de fclose não pode ser evitada de qualquer maneira. Portanto, não podemos poupar o cliente do incômodo de liberar (fechar) o recurso expondo a definição de FILE e retornando-o pelo valor em fopen ou espere muito de um aumento de desempenho dado o custo relativo das próprias operações de arquivo para evitar uma alocação de heap.

Pontos de acesso e correções

No entanto, para outros casos, eu “fiz o perfil de muitos códigos C inúteis em bases de código legadas com pontos de acesso em malloc e perdas de cache obrigatórias desnecessárias como resultado do uso dessa prática com muita frequência com ponteiros opacos e alocação de muitas coisas desnecessariamente no heap, às vezes em grandes loops.

Uma prática alternativa que uso em vez disso é expor as definições da estrutura, mesmo que o cliente não pretenda alterá-las, 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 problemas de compatibilidade binária no futuro, então eu achei bom o suficiente para apenas reservar supérfluo algum espaço extra para fins 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 desperdício, mas pode salvar uma vida se descobrirmos no futuro que precisamos adicionar mais alguns dados a Foo sem quebrar os binários que usam nossa biblioteca.

Na minha opinião, ocultação de informações e compatibilidade binária é normalmente a única razão decente para permitir apenas a alocação de heap de estruturas além de estruturas de comprimento variável (o que sempre exigiria isso, ou pelo menos seria um pouco estranho de usar, caso o cliente tivesse que alocar memória na pilha em um fash de VLA para alocar o VLS). Mesmo as estruturas grandes costumam ser mais baratas para retornar por valor, se isso significar que o software está funcionando muito mais com a memória ativa na pilha. E mesmo que não fosse mais barato retornar por valor na criação, poderia simplesmente fazer isso:

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.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *