¿Debo inicializar C structs a través de parámetros o por valor de retorno? [cerrado]

33

La compañía en la que trabajo está inicializando todas sus estructuras de datos a través de una función de inicialización como esta:

//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);

Obtuve algunos intentos de inicializar mis estructuras de esta manera:

//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);

¿Hay alguna ventaja para uno sobre el otro?
¿Cuál debería hacer y cómo lo justificaría como una mejor práctica?

Trevor Hickey
fuente
44
@gnat Esta es una pregunta explícita sobre la inicialización de la estructura. Ese hilo encarna parte de la misma lógica que me gustaría ver aplicada para esta decisión de diseño en particular.
Trevor Hickey
2
@Jefffrey Estamos en C, por lo que no podemos tener métodos. Tampoco es siempre un conjunto directo de valores. A veces, inicializar una estructura es obtener valores (de alguna manera), y realiza algo de lógica para inicializar la estructura.
Trevor Hickey
1
@JacquesB Obtuve "Cada componente que construyas será diferente a los demás. Hay una función Initialize () utilizada en otro lugar para la estructura. Técnicamente hablando, llamarlo constructor es engañoso".
Trevor Hickey
1
@TrevorHickey InitializeFoo()es un constructor. La única diferencia con un constructor de C ++ es que el thispuntero se pasa explícitamente en lugar de implícitamente. El código compilado de InitializeFoo()y un C ++ correspondiente Foo::Foo()es exactamente el mismo.
cmaster
2
Mejor opción: dejar de usar C sobre C ++. Autowin.
Thomas Eding

Respuestas:

25

En el segundo enfoque, nunca tendrás un Foo medio inicializado. Poner toda la construcción en un solo lugar parece un lugar más sensible y obvio.

Pero ... la primera forma no es tan mala, y a menudo se usa en muchas áreas (incluso hay una discusión sobre la mejor forma de inyectar dependencia, ya sea la inyección de propiedades como la primera forma o la inyección del constructor como la segunda) . Tampoco está mal.

Entonces, si ninguno de los dos está mal y el resto de la compañía usa el enfoque n. ° 1, entonces debería ajustarse a la base de código existente y no tratar de estropearla introduciendo un nuevo patrón. Este es realmente el factor más importante en el juego aquí, juega bien con tus nuevos amigos y no trates de ser ese copo de nieve especial que hace las cosas de manera diferente.

gbjbaanb
fuente
Ok, parece razonable. Tenía la impresión de que inicializar un objeto sin poder ver qué tipo de entrada lo estaba inicializando, podría generar confusión. Estaba tratando de seguir el concepto de entrada / salida de datos para producir código predecible y comprobable. Hacerlo de la otra manera parecía aumentar el acoplamiento ya que el archivo fuente de mi estructura necesitaba dependencias adicionales para realizar la inicialización. Sin embargo, tienes razón, ya que no quiero mover el bote a menos que una forma sea altamente preferida sobre otra.
Trevor Hickey
44
@TrevorHickey: En realidad, diría que hay dos diferencias clave entre los ejemplos que da: (1) En uno, la función pasa un puntero a la estructura para inicializar, y en el otro devuelve una estructura inicializada; (2) En uno, los parámetros de inicialización se pasan a la función, y en el otro son implícitos. Parece que está preguntando más sobre (2), pero las respuestas aquí se concentran en (1). Es posible que desee aclarar que - Sospecho que la mayoría de la gente recomendaría un híbrido de los dos usando parámetros explícitos y un puntero:void SetupFoo(Foo *out, int a, int b, int c)
psmears
1
¿Cómo conduciría el primer enfoque a una Fooestructura "semi-inicializada" ? El primer enfoque también realiza toda la inicialización en un solo lugar. (¿O está considerando que una estructura no inicializada Fooestá "
semiinicializada
1
@jamesdlin en los casos en que se crea el Foo y se omite accidentalmente InitialiseFoo. Fue solo una forma de hablar para describir la inicialización de 2 fases sin escribir una descripción larga. Me imaginé que las personas con un tipo de desarrollador experimentado lo entenderían.
gbjbaanb
22

Ambos enfoques agrupan el código de inicialización en una sola llamada de función. Hasta aquí todo bien.

Sin embargo, hay dos problemas con el segundo enfoque:

  1. El segundo no construye realmente el objeto resultante, inicializa otro objeto en la pila, que luego se copia al objeto final. Es por eso que vería el segundo enfoque como ligeramente inferior. El retroceso que ha recibido probablemente se deba a esta copia extraña.

    Esto es aún peor cuando deriva una clase Derivedde Foo(las estructuras se usan principalmente para la orientación de objetos en C): con el segundo enfoque, la función ConstructDerived()llamaría ConstructFoo(), copie el Fooobjeto temporal resultante en la ranura de la superclase de un Derivedobjeto; terminar la inicialización del Derivedobjeto; solo para que el objeto resultante se vuelva a copiar al volver. Agregue una tercera capa, y todo se vuelve completamente ridículo.

  2. Con el segundo enfoque, las ConstructClass()funciones no tienen acceso a la dirección del objeto en construcción. Esto hace que sea imposible vincular objetos durante la construcción, ya que es necesario cuando un objeto necesita registrarse con otro objeto para una devolución de llamada.


Finalmente, no todas structsson clases completas. Algunos structsefectivamente solo agrupan un conjunto de variables, sin restricciones internas a los valores de estas variables. typedef struct Point { int x, y; } Point;Sería un buen ejemplo de esto. Para estos el uso de una función inicializadora parece excesivo. En estos casos, la sintaxis literal compuesta puede ser conveniente (es C99):

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

o

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

    return (Point){ .x = n, .y = n*n };
}
cmaster
fuente
55
No pensé que las copias serían un problema debido a en.wikipedia.org/wiki/Copy_elision
Trevor Hickey
55
Que el compilador pueda eludir la copia no alivia el hecho de que usted haya escrito la copia. En C, escribir operaciones superfluas y confiar en el compilador para corregirlas se considera un mal estilo. Esto es diferente de C ++, donde la gente se enorgullece cuando puede demostrar que el compilador teóricamente puede eliminar toda la información que dejan sus plantillas anidadas. En C, las personas intentan escribir exactamente el código que la máquina debería ejecutar. De todos modos, el punto sobre las direcciones inaccesibles sigue siendo, copiar elision no puede ayudarlo allí.
cmaster
3
Cualquiera que use un compilador debe esperar que el código que escriba sea transformado por el compilador. A menos que estén ejecutando un intérprete de hardware C, el código que escriben no será el código que ejecutan, incluso si es fácil creer lo contrario. Si entienden su compilador, entenderán la elisión, y no es diferente a int x = 3;no almacenar la cadena xen el binario. La dirección y los puntos de herencia son buenos; El supuesto fracaso de la elisión es una tontería.
Yakk
@Yakk: Históricamente, C fue inventado para servir como una forma de lenguaje ensamblador de alto nivel para la programación de sistemas. En los años posteriores, su identidad se ha vuelto cada vez más turbia. Algunas personas quieren que sea un lenguaje de aplicación optimizado, pero dado que no ha surgido una mejor forma de lenguaje ensamblador de alto nivel, C todavía es necesario para cumplir con este último rol. No veo nada de malo en la idea de que un código de programa bien escrito debería comportarse al menos decentemente, incluso cuando se compila con optimizaciones mínimas, aunque para que eso realmente funcione requeriría que C agregue algunas cosas de las que ha faltado durante mucho tiempo.
supercat
@Yakk: Tener directivas, por ejemplo, que le dirían a un compilador "Las siguientes variables se pueden mantener de forma segura en los registros durante el siguiente tramo de código", así como un medio para copiar en bloque un tipo que no sea unsigned char no sea permitir optimizaciones para las cuales el La regla de alias estricta sería insuficiente, al tiempo que aclara las expectativas del programador.
supercat
1

Dependiendo del contenido de la estructura y del compilador particular que se utilice, cualquier enfoque podría ser más rápido. Un patrón típico es que las estructuras que cumplen ciertos criterios pueden ser devueltas en los registros; para las funciones que devuelven otros tipos de estructura, el llamante debe asignar espacio para la estructura temporal en algún lugar (generalmente en la pila) y pasar su dirección como un parámetro "oculto"; En los casos en que el retorno de una función se almacena directamente en una variable local cuya dirección no se encuentra en ningún código externo, algunos compiladores pueden pasar la dirección de esa variable directamente.

Si un tipo de estructura satisface los requisitos de una implementación particular para que se devuelva en registros (por ejemplo, que no sea más grande que una palabra de máquina, o que complete exactamente dos palabras de máquina) que tenga una función de retorno, la estructura puede ser más rápida que pasar la dirección de una estructura, especialmente ya que exponer la dirección de una variable al código externo que podría mantener una copia de ella podría impedir algunas optimizaciones útiles. Si un tipo no satisface tales requisitos, el código generado para una función que devuelve una estructura será similar al de una función que acepta un puntero de destino; el código de llamada probablemente sería más rápido para el formulario que toma un puntero, pero ese formulario podría perder algunas oportunidades de optimización.

Es una pena que C no proporcione un medio para decir que una función tiene prohibido guardar una copia de un puntero pasado (semántica similar a una referencia de C ++) ya que pasar un puntero restringido obtendría las ventajas de rendimiento directo de pasar un puntero a un objeto preexistente, pero al mismo tiempo evite los costos semánticos de requerir que un compilador considere la dirección de una variable "expuesta".

Super gato
fuente
3
Para su último punto: no hay nada en C ++ que impida que una función guarde una copia de un puntero que se pasa como referencia, la función simplemente puede tomar la dirección del objeto. También es libre de usar la referencia para construir otro objeto que contenga esa referencia (no se creó un puntero desnudo). Sin embargo, la copia del puntero o la referencia en el objeto pueden sobrevivir al objeto al que apuntan, creando un puntero / referencia colgante. Entonces, el punto sobre la seguridad de referencia es bastante mudo.
cmaster
@cmaster: en plataformas que devuelven estructuras pasando un puntero al almacenamiento temporal, los compiladores de C no proporcionan funciones llamadas con ninguna forma de acceder a la dirección de ese almacenamiento. En C ++, es posible obtener la dirección de una variable pasada por referencia, pero a menos que la persona que llama garantice la vida útil del elemento pasado (en cuyo caso, por lo general, habría pasado un puntero), probablemente se producirá un comportamiento indefinido.
supercat
1

Un argumento a favor del estilo "parámetro de salida" es que permite que la función devuelva un código de error.

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!
}

Teniendo en cuenta algún conjunto de estructuras relacionadas, si la inicialización puede fallar para cualquiera de ellas, puede valer la pena hacer que todas usen el estilo de fuera del parámetro por razones de coherencia.

Viktor Dahl
fuente
1
Aunque uno solo podría establecer errno.
Deduplicador
0

Supongo que su enfoque está en la inicialización a través del parámetro de salida frente a la inicialización a través del retorno, no la discrepancia en la forma en que se proporcionan los argumentos de construcción.

Tenga en cuenta que el primer enfoque podría permitir Fooser opaco (aunque no con la forma en que lo usa actualmente), y eso generalmente es deseable para la mantenibilidad a largo plazo. Podría considerar, por ejemplo, una función que asigna una Fooestructura opaca sin inicializarla. O tal vez necesite reiniciar una Fooestructura que se inicializó previamente con diferentes valores.

jamesdlin
fuente
Downvoter, ¿quieres explicarlo? ¿Es algo que dije es incorrecto?
jamesdlin