Devo inicializar estruturas C via parâmetro ou pelo valor de retorno? [fechadas]

33

A empresa em que trabalho está inicializando todas as estruturas de dados por meio de uma função de inicialização da seguinte maneira:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Eu recebi um empurrão para trás tentando inicializar minhas estruturas assim:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Existe uma vantagem para um sobre o outro?
Qual deles devo fazer e como justificaria isso como uma prática melhor?

Trevor Hickey
fonte
4
@gnat Esta é uma pergunta explícita sobre a inicialização do struct. Esse tópico incorpora algumas das mesmas justificativas que eu gostaria de ver aplicadas a essa decisão de design específica.
Trevor Hickey
2
@ Jeffffrey Estamos em C, então não podemos realmente ter métodos. Nem sempre é um conjunto direto de valores. Às vezes, inicializar uma estrutura é obter valores (de alguma forma) e executar alguma lógica para inicializar a estrutura.
Trevor Hickey
1
@ JacquesB eu recebi "Cada componente que você constrói será diferente dos outros. Existe uma função Initialize () usada em outro lugar para a estrutura. Tecnicamente falando, chamá-lo de construtor é enganoso."
Trevor Hickey
1
@TrevorHickey InitializeFoo()é um construtor. A única diferença de um construtor C ++ é que o thisponteiro é passado explicitamente e não implicitamente. O código compilado InitializeFoo()e um C ++ correspondente Foo::Foo()são exatamente iguais.
#
2
Melhor opção: pare de usar C sobre C ++. Autowin.
Thomas Eding

Respostas:

25

Na segunda abordagem, você nunca terá um Foo semi-inicializado. Colocar toda a construção em um só lugar parece um lugar mais sensato e óbvio.

Mas ... a 1ª via não é tão ruim e é frequentemente usada em muitas áreas (há até uma discussão sobre a melhor maneira de injetar dependências, injeção de propriedades como sua primeira via ou injeção de construtores como a segunda) . Nem está errado.

Portanto, se nenhum dos dois estiver errado e o resto da empresa usar a abordagem nº 1, você deverá se ajustar à base de código existente e não tentar atrapalhar a introdução de um novo padrão. Este é realmente o fator mais importante em jogo aqui, seja agradável com seus novos amigos e não tente ser aquele floco de neve especial que faz as coisas de maneira diferente.

gbjbaanb
fonte
Ok, parece razoável. Fiquei com a impressão de que inicializar um objeto sem poder ver que tipo de entrada o inicializava, levaria à confusão. Eu estava tentando seguir o conceito de entrada / saída de dados para produzir código previsível e testável. Fazer isso de outra maneira parecia aumentar o acoplamento, pois o arquivo de origem da minha estrutura precisava de dependências extras para executar a inicialização. Você está certo, porém, no sentido de que não quero agitar o barco, a menos que uma maneira seja altamente preferida à outra.
Trevor Hickey
4
@TrevorHickey: Na verdade, eu diria que existem duas diferenças principais entre os exemplos que você fornece - (1) Em uma, a função passa um ponteiro para a estrutura para inicializar e, na outra, retorna uma estrutura inicializada; (2) Em um, os parâmetros de inicialização são passados ​​para a função e, no outro, estão implícitos. Você parece estar perguntando mais sobre (2), mas as respostas aqui estão concentradas em (1). Você pode querer esclarecer que - Eu suspeito que a maioria das pessoas gostaria de recomendar um híbrido dos dois usando parâmetros explícitos e um ponteiro:void SetupFoo(Foo *out, int a, int b, int c)
psmears
1
Como a primeira abordagem levaria a uma estrutura "semi-inicializada" Foo? A primeira abordagem também realiza toda a inicialização em um único local. (Ou você está considerando uma un inicializado Foostruct para ser "meio-inicializado"?)
jamesdlin
1
@jamesdlin nos casos em que o Foo é criado e o InitialiseFoo é acidentalmente perdido. Era apenas uma figura de linguagem para descrever a inicialização em duas fases sem digitar uma descrição longa. Imaginei pessoas experientes do tipo desenvolvedor que entenderiam.
gbjbaanb
22

Ambas as abordagens agrupam o código de inicialização em uma única chamada de função. Por enquanto, tudo bem.

No entanto, existem dois problemas com a segunda abordagem:

  1. O segundo não constrói o objeto resultante, ele inicializa outro objeto na pilha, que é copiado para o objeto final. É por isso que eu consideraria a segunda abordagem um pouco inferior. A resposta que você recebeu provavelmente deve-se a esta cópia estranha.

    Isto é ainda pior quando você derivar uma classe Derivedde Foo(estruturas são largamente utilizados para orientação a objetos em C): Com a segunda abordagem, a função ConstructDerived()chamaria ConstructFoo(), copie o temporária resultando Fooobjeto sobre na ranhura superclasse de um Derivedobjeto; terminar a inicialização do Derivedobjeto; apenas para que o objeto resultante seja copiado novamente no retorno. Adicione uma terceira camada, e a coisa toda se torna completamente ridícula.

  2. Com a segunda abordagem, as ConstructClass()funções não têm acesso ao endereço do objeto em construção. Isso torna impossível vincular objetos durante a construção, pois é necessário quando um objeto precisa se registrar com outro objeto para um retorno de chamada.


Finalmente, nem todas structssão classes de pleno direito. Alguns structsefetivamente agrupam várias variáveis, sem nenhuma restrição interna aos valores dessas variáveis. typedef struct Point { int x, y; } Point;seria um bom exemplo disso. Para estes, o uso de uma função inicializadora parece um exagero. Nesses casos, a sintaxe literal composta pode ser conveniente (é C99):

Point = { .x = 7, .y = 9 };

ou

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}
cmaster
fonte
5
Não achei que as cópias fossem um problema por causa de en.wikipedia.org/wiki/Copy_elision #
Trevor Hickey
5
O fato de o compilador poder excluir a cópia não alivia o fato de você ter anotado a cópia. Em C, escrever operações supérfluas e confiar no compilador para corrigi-las é considerado um estilo ruim. Isso é diferente do C ++, onde as pessoas se orgulham quando conseguem provar que o compilador pode teoricamente remover todo o lixo deixado pelos modelos aninhados. Em C, as pessoas tentam escrever exatamente o código que a máquina deve executar. De qualquer forma, o argumento sobre os endereços inacessíveis permanece: a cópia elision não pode ajudá-lo lá.
#
3
Qualquer pessoa que use um compilador deve esperar que o código que ele escreve seja transformado pelo compilador. A menos que eles estejam executando um interpretador C de hardware, o código que eles escreverem não será o código que eles executam, mesmo que seja fácil acreditar no contrário. Se eles entenderem seu compilador, entenderão elision, e não é diferente de int x = 3;não armazenar a string xno binário. O endereço e os pontos de herança são bons; o suposto fracasso da elisão é tolice.
Yakk
@Yakk: Historicamente, o C foi inventado para servir como uma forma de linguagem assembly de alto nível para programação de sistemas. Nos anos seguintes, sua identidade se tornou cada vez mais sombria. Algumas pessoas querem que seja uma linguagem de aplicativo otimizada, mas como não surgiu uma melhor forma de linguagem assembly de alto nível, C ainda é necessário para desempenhar esse último papel. Não vejo nada de errado com a ideia de que um código de programa bem escrito deva se comportar pelo menos decentemente, mesmo quando compilado com otimizações mínimas, mas para fazer isso realmente funcionar seria necessário que C adicionasse algumas coisas que há muito falta.
23715
@Yakk: Ter diretivas, por exemplo, que diriam a um compilador "As seguintes variáveis ​​podem ser mantidas em registros com segurança durante o seguinte trecho de código", bem como um meio de copiar um tipo de bloco que unsigned charnão permita otimizações para as quais o A regra estrita de aliasing seria insuficiente, ao mesmo tempo em que tornava as expectativas do programador mais claras.
Supercat
1

Dependendo do conteúdo da estrutura e do compilador específico que está sendo usado, qualquer uma das abordagens pode ser mais rápida. Um padrão típico é que estruturas que atendem a certos critérios podem ser retornadas em registros; para funções que retornam outros tipos de estrutura, o chamador deve alocar espaço para a estrutura temporária em algum lugar (geralmente na pilha) e transmitir seu endereço como um parâmetro "oculto"; nos casos em que o retorno de uma função é armazenado diretamente em uma variável local cujo endereço não é mantido por nenhum código externo, alguns compiladores podem passar o endereço dessa variável diretamente.

Se um tipo de estrutura satisfaz os requisitos de uma determinada implementação a serem retornados nos registros (por exemplo, não sendo maior que uma palavra-máquina ou preenchendo precisamente duas palavras-máquina) com uma função de retorno, a estrutura pode ser mais rápida do que passar o endereço de uma estrutura, especialmente como a exposição do endereço de uma variável ao código externo que pode manter uma cópia dela pode impedir algumas otimizações úteis. Se um tipo não atender a esses requisitos, o código gerado para uma função que retorna uma estrutura será semelhante ao de uma função que aceita um ponteiro de destino; o código de chamada provavelmente seria mais rápido para o formulário que usa um ponteiro, mas esse formulário perde algumas oportunidades de otimização.

É uma pena que C não forneça um meio de dizer que uma função é proibida de manter uma cópia de um ponteiro passado (semântica semelhante a uma referência C ++), pois a passagem de um ponteiro restrito obteria as vantagens diretas de desempenho da passagem um ponteiro para um objeto preexistente, mas, ao mesmo tempo, evite os custos semânticos de exigir que um compilador considere "exposto" o endereço de uma variável.

supercat
fonte
3
Até o seu último ponto: não há nada no C ++ que impeça uma função de manter uma cópia de um ponteiro transmitido como referência; a função pode simplesmente pegar o endereço do objeto. Também é livre usar a referência para construir outro objeto que contenha essa referência (nenhum ponteiro nu criado). No entanto, a cópia do ponteiro ou a referência no objeto podem sobreviver ao objeto para o qual apontam, criando um ponteiro / referência pendente. Portanto, a questão da segurança de referência é bastante muda.
21/15
@ master: Em plataformas que retornam estruturas passando um ponteiro para armazenamento temporário, os compiladores C não fornecem funções chamadas com nenhuma maneira de acessar o endereço desse armazenamento. Em C ++, é possível obter o endereço de uma variável passada por referência, mas, a menos que o chamador garanta a vida útil do item passado (nesse caso, normalmente ele passaria um ponteiro), provavelmente ocorrerá um comportamento indefinido.
Supercat
1

Um argumento a favor do estilo "parâmetro de saída" é que ele permite que a função retorne um código de erro.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

Considerando um conjunto de estruturas relacionadas, se a inicialização puder falhar para qualquer uma delas, pode valer a pena que todas elas usem o estilo out-parameter por uma questão de consistência.

Viktor Dahl
fonte
1
Embora se possa apenas definir errno.
Deduplicator 21/07
0

Presumo que seu foco esteja na inicialização via parâmetro de saída vs. inicialização via retorno, não na discrepância de como os argumentos de construção são fornecidos.

Observe que a primeira abordagem pode permitir Fooser opaca (embora não seja da maneira que você a usa atualmente), e isso geralmente é desejável para manutenção a longo prazo. Você pode considerar, por exemplo, uma função que aloca uma Fooestrutura opaca sem inicializá-la. Ou talvez você precise reinicializar uma Fooestrutura que foi inicializada anteriormente com valores diferentes.

jamesdlin
fonte
Downvoter, gostaria de explicar? Algo que eu disse factualmente incorreto?
Jamesdlin