¿Es malo escribir C orientado a objetos? [cerrado]

14

Siempre parece que escribo código en C que está orientado principalmente a objetos, así que digamos que tenía un archivo fuente o algo para crear una estructura y luego pasar el puntero a esta estructura a las funciones (métodos) que posee esta estructura:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

¿Es esta una mala práctica? ¿Cómo aprendo a escribir C de la "manera correcta"?

mosmo
fuente
10
Gran parte de Linux (el núcleo) está escrito de esta manera, de hecho, incluso emula aún más conceptos similares a OO como el envío de métodos virtuales. Lo considero bastante apropiado.
Kilian Foth
13
" [E] determinó que Real Programmer puede escribir programas FORTRAN en cualquier idioma " . - Ed Post, 1983
Ross Patterson
44
¿Hay alguna razón por la que no quieres cambiar a C ++? No tiene que usar las partes que no le gustan.
svick
55
Esto realmente plantea la pregunta de "¿Qué es 'orientado a objetos'?" No llamaría a este objeto orientado. Yo diría que es de procedimiento. (No tiene herencia, ni polimorfismo, ni encapsulación / capacidad para ocultar el estado, y probablemente le faltan otras características distintivas de OO que no salen de la parte superior de mi cabeza). Si la buena o mala práctica no depende de esa semántica , aunque.
jpmc26
3
@ jpmc26: Si usted es un prescriptivista lingüístico, debe escuchar a Alan Kay, él inventó el término, puede decir lo que significa y dice que OOP se trata de mensajería . Si usted es un descriptivista lingüístico, encuestará el uso del término en la comunidad de desarrollo de software. Cook hizo exactamente eso, analizó las características de los idiomas que afirman o se consideran OO, y descubrió que tienen una cosa en común: la mensajería .
Jörg W Mittag

Respuestas:

24

No, esto no es una mala práctica, incluso se recomienda hacerlo, aunque incluso se podrían usar convenciones como struct foo *foo_new();yvoid foo_free(struct foo *foo);

Por supuesto, como dice un comentario, solo haga esto cuando sea apropiado. No tiene sentido usar un constructor para un int.

El prefijo foo_es una convención seguida de muchas bibliotecas, ya que protege contra choques con el nombre de otras bibliotecas. Otras funciones a menudo tienen la convención de usar foo_<function>(struct foo *foo, <parameters>);. Esto le permite struct fooser de tipo opaco.

Eche un vistazo a la documentación de libcurl para la convención, especialmente con "subnombres de espacios", de modo que llamar a una función se curl_multi_*vea mal a primera vista cuando el primer parámetro fue devuelto por curl_easy_init().

Hay enfoques aún más genéricos, consulte Programación orientada a objetos con ANSI-C

Residuo
fuente
11
Siempre con la advertencia "cuando corresponda". OOP no es una bala de plata.
Deduplicador
¿C no tiene espacios de nombres en los que podría declarar estas funciones? Similar a std::string, ¿no podrías tener foo::create? No uso C. ¿Tal vez eso es solo en C ++?
Chris Cirefice
@ChrisCirefice No hay espacios de nombres en C, por eso muchos autores de bibliotecas usan prefijos para sus funciones.
Residuo
2

No está mal, es excelente. Programación Orientada a Objetos es una buena cosa (a menos que se deje llevar, que puede tener demasiado de algo bueno). C no es el lenguaje más adecuado para OOP, pero eso no debería impedir que aproveches al máximo.

gnasher729
fuente
44
No es que pueda estar en desacuerdo, pero su opinión realmente debería estar respaldada por alguna elaboración.
Deduplicador
1

No está mal. Respalda el uso de RAII que evita muchos errores (pérdidas de memoria, uso de variables no inicializadas, uso después de free, etc., que pueden causar problemas de seguridad).

Entonces, si desea compilar su código solo con GCC o Clang (y no con el compilador de MS), puede usar el cleanupatributo, que destruirá adecuadamente sus objetos. Si declaras tu objeto así:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Luego my_str_destructor(ptr)se ejecutará cuando ptr esté fuera de alcance. Solo tenga en cuenta que no se puede usar con argumentos de función .

Además, recuerde usar my_str_los nombres de sus métodos, porque Cno tiene espacios de nombres y es fácil colisionar con algún otro nombre de función.

Marqin
fuente
2
Afaik, RAII se trata de usar la llamada implícita de destructores para objetos en C ++ para garantizar la limpieza, evitando la necesidad de agregar llamadas explícitas de liberación de recursos. Entonces, si no me equivoco mucho, RAII y C son mutuamente excluyentes.
cmaster - reinstalar a monica el
@cmaster Si usa #definesus nombres de tipo __attribute__((cleanup(my_str_destructor))), lo obtendrá como implícito en todo el #definealcance (se agregará a todas sus declaraciones de variables).
Marqin
Eso funciona si a) usa gcc, b) si usa el tipo solo en variables locales de función, yc) si usa el tipo solo en una versión desnuda (sin punteros al #definetipo 'd o matrices de este). En resumen: no es el estándar C, y paga con mucha inflexibilidad en uso.
cmaster - reinstalar a monica
Como mencioné en mi respuesta, eso también funciona en clang.
Marqin
Ah, no me di cuenta de eso. De hecho, eso hace que el requisito a) sea significativamente menos severo, ya que eso lo convierte en __attribute__((cleanup()))un cuasi-estándar. Sin embargo, b) yc) siguen en pie ...
cmaster - reinstalar a monica
-2

Puede haber muchas ventajas en dicho código, pero desafortunadamente el Estándar C no fue escrito para facilitarlo. Históricamente, los compiladores han ofrecido garantías de comportamiento efectivas más allá de lo que requería el Estándar, lo que hizo posible escribir dicho código de manera mucho más limpia de lo que es posible en el Estándar C, pero los compiladores han comenzado recientemente a revocar tales garantías en nombre de la optimización.

En particular, muchos compiladores de C han garantizado históricamente (por diseño, si no documentación) que si dos tipos de estructura contienen la misma secuencia inicial, se puede usar un puntero a cualquiera de los tipos para acceder a los miembros de esa secuencia común, incluso si los tipos no están relacionados, y además que para propósitos de establecer una secuencia inicial común, todos los apuntadores a estructuras son equivalentes. El código que hace uso de este comportamiento puede ser mucho más limpio y más seguro de tipo que el código que no lo hace, pero desafortunadamente, aunque el Estándar requiere que las estructuras que comparten una secuencia inicial común deben establecerse de la misma manera, prohíbe que el código realmente use un puntero de un tipo para acceder a la secuencia inicial de otro.

En consecuencia, si desea escribir código orientado a objetos en C, tendrá que decidir (y debe tomar esta decisión desde el principio) saltar entre muchos aros para cumplir con las reglas de tipo puntero de C y estar preparado para tener los compiladores modernos generan código sin sentido si uno se desliza hacia arriba, incluso si los compiladores más antiguos hubieran generado un código que funciona según lo previsto, o de lo contrario documentan el requisito de que el código solo se pueda usar con compiladores que estén configurados para admitir el comportamiento del puntero de estilo antiguo (por ejemplo, usando un "-fno-estricto-alias") Algunas personas consideran que "-fno-estricto-aliasing" es malo, pero sugeriría que es más útil pensar en "-fno-estricto-aliasing" C como un lenguaje que ofrece mayor poder semántico para algunos propósitos que el "estándar" C,pero a expensas de optimizaciones que pueden ser importantes para otros propósitos.

A modo de ejemplo, en los compiladores tradicionales, los compiladores históricos interpretarían el siguiente código:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

siguiendo los siguientes pasos en orden: incremente el primer miembro de *p, complemente el bit más bajo del primer miembro de *t, luego disminuya el primer miembro de *py complemente el bit más bajo del primer miembro de *t. Los compiladores modernos reorganizarán la secuencia de operaciones de una manera que codifique, que será más eficiente pe tidentificará diferentes objetos, pero cambiará el comportamiento si no lo hacen.

Este ejemplo es, por supuesto, deliberadamente ideado, y en la práctica el código que usa un puntero de un tipo para acceder a miembros que son parte de la secuencia inicial común de otro tipo generalmente funcionará, pero desafortunadamente ya que no hay forma de saber cuándo podría fallar dicho código no es posible usarlo de manera segura, excepto deshabilitando el análisis de alias basado en tipo.

Un ejemplo algo menos artificial ocurriría si uno quisiera escribir una función para hacer algo como cambiar dos punteros a tipos arbitrarios. En la gran mayoría de los compiladores "C de la década de 1990", eso podría lograrse mediante:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

En el Estándar C, sin embargo, uno debería usar:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Si *p2se mantiene en el almacenamiento asignado y el puntero temporal no se mantiene en el almacenamiento asignado, el tipo efectivo de *p2se convertirá en el tipo del puntero temporal y el código que intenta usar *p2como cualquier tipo que no coincida con el puntero temporal type invocará Comportamiento indefinido. Es seguro que es extremadamente improbable que un compilador se dé cuenta de tal cosa, pero dado que la filosofía moderna del compilador requiere que los programadores eviten el Comportamiento Indefinido a toda costa, no puedo pensar en ningún otro medio seguro de escribir el código anterior sin usar el almacenamiento asignado .

Super gato
fuente
Votantes: ¿Te gustaría comentar? Un aspecto clave de la programación orientada a objetos es la capacidad de hacer que múltiples tipos compartan aspectos comunes, y tener un puntero a cualquiera de estos tipos para poder acceder a esos aspectos comunes. El ejemplo del OP no hace eso, pero apenas rasca la superficie de estar "orientado a objetos". Los compiladores históricos de C permitirían que el código polimórfico se escribiera de una manera mucho más limpia de lo que es posible en el estándar actual C. El diseño de código orientado a objetos en C requiere que uno determine a qué lenguaje exacto se dirige. ¿Con qué aspecto la gente no está de acuerdo?
supercat
Hm ... ¿te importa mostrar cómo las garantías que ofrece el estándar no te permiten acceder limpiamente a los miembros de la subsecuencia inicial común? ¿Porque creo que de eso depende tu maldad acerca de los males de atreverse a optimizar dentro de los límites del comportamiento contractual? (Esa es mi suposición de lo que los dos downvoters encontraron objetable.)
Deduplicador
OOP no requiere necesariamente herencia, por lo que la compatibilidad entre dos estructuras no es un gran problema en la práctica. Puedo obtener OO real al poner punteros de función en una estructura e invocar esas funciones de una manera particular. Por supuesto, este foo_function(foo*, ...)pseudo-OO en C es solo un estilo de API particular que parece clases, pero debería llamarse más adecuadamente programación modular con tipos de datos abstractos.
amon
@Dupuplicator: Vea el ejemplo indicado. El campo "i1" es un miembro de la secuencia inicial común de ambas estructuras, pero los compiladores modernos fallarán si el código intenta usar un "par de estructura *" para acceder al miembro inicial de un "trío de estructura".
supercat
¿Qué compilador de C moderno falla ese ejemplo? ¿Se necesitan opciones interesantes?
Deduplicador
-3

El siguiente paso es ocultar la declaración de estructura. Pones esto en el archivo .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

Y luego en el archivo .c:

struct foo_s {
    ...
};
Salomón lento
fuente
66
Ese podría ser el siguiente paso, dependiendo del objetivo. Sin importar si es o no, esto ni siquiera trata de responder la pregunta de forma remota.
Deduplicador