Абстракции и наследование в Си — стреляем по ногам красиво

от автора

TL;DR https://github.com/pomidoroshev/c-inheritance

Иногда нет-нет да и хочется что-нибудь абстрагировать и обобщить в коде на Си. К примеру, хочешь ты принтануть содержимое структуры несколько раз, пишешь везде, как дурак, printf("%s %d %f\n", foo->bar, foo->baz, foo->boom), и интуитивно кажется, что есть способ сделать foo->print(foo), и так вообще со всеми структурами, не только с foo.

Возьмем пример: есть некий чувак с именем и фамилией, и есть птица, у которой есть имя и владелец.

typedef struct Person Person; struct Person {     char *first_name;     char *last_name; };  typedef struct Bird Bird; struct Bird {     char *name;     Person *owner; };

Чтобы вывести информацию про этих животных, кондовый сишник напишет просто две функции:

void Person_Print(Person *p) {     printf("%s %s\n", p->first_name, p->last_name); }  void Bird_Print(Bird *b) {     printf("%s of %s %s\n", b->name, b->owner->first_name, b->owner->last_name); }

И будет таки прав! Но что если подобных структур у нас много, а наш мозг испорчен веяниями ООП? Правильно, надо у каждой структуры определить общий метод, например void Repr(Person* person, char* buf), который сбросит в buf строковое представление объекта (да, теперь у нас появляются объекты), и дальше мы бы могли использовать этот результат для вывода на экран:

/* Person */ struct Person {     void (*Repr)(Person*, char*);     /* ... */ };  void Person_Repr(Person *person, char *buf) {     sprintf(buf, "<Person: first_name='%s' last_name='%s'>",             person->first_name, person->last_name); }  Person *New_Person(char *first_name, char *last_name) {     Person *person = malloc(sizeof(Person));     person->Repr = Person_Repr;     person->first_name = first_name;     person->last_name = last_name;     return person; }  /* Bird */ struct Bird {     void (*Repr)(Bird*, char*);     /* ... */ };  void Bird_Repr(Bird *bird, char* buf) {     char owner_repr[80];     bird->owner->Repr(bird->owner, owner_repr);     sprintf(buf, "<Bird: name='%s' owner=%s>",             bird->name, owner_repr); }  Bird *New_Bird(char *name, Person *owner) {     Bird *bird = malloc(sizeof(Bird));     bird->Repr = Bird_Repr;     bird->name = name;     bird->owner = owner;     return bird; }

Окей, вроде унифицировали, да не очень. Как теперь эти методы вызывать? Не очень удобно получается, каждый раз вылезает свистопляска с буферами:

char buf[80]; bird->Repr(bird, buf); printf("%s\n", buf);

Как вариант — сделать базовую структуру Object, положить в нее функцию Print(), «наследовать» остальные структуры от Object и в Object::Print() дергать дочерний метод Repr(). Выглядит логично, только мы пишем на Си, а не на плюсах, где такое на раз-два решается виртуальными функциями.

Но в Си есть такая штука: можно привести одну структуру к другой, если у нее та другая структура идет первым полем.

Например:

typedef struct {     int i; } Foo;  typedef struct {     Foo foo;     int j; } Bar;  Bar *bar = malloc(sizeof(Bar)); bar->foo.i = 123; printf("%d\n", ((Foo*)bar)->i);

То есть мы смотрим на структуру bar, но с типом Foo, потому что по сути указатель на структуру — это указатель на ее первый элемент, и тут мы имеем право так кастовать.

Попробуем сделать базовую структуру Object с одной функцией Print_Repr(), которая, по идее, должна будет вызвать «дочерний метод» Repr() у наших людишек и птичек:

typedef struct Object Object; struct Object {     void (*Print_Repr)(Object*); };  /*  Самая интересная часть. Функция берет указатель  на следующее поле в структуре после Object,  которое в текущем варианте является указателем  на функцию Repr().  */ void Object_Print_Repr(Object *object) {     void **p_repr_func = (void*) object + sizeof(Object);     void (*repr_func)(Object*, char*) = *p_repr_func;     char buf[80];     repr_func(object, buf);     printf("%s\n", buf); }  /* Person */ typedef struct Person Person; struct Person {     Object object;     void (*Repr)(Person*, char*);     /* ... */ };  Person *New_Person(char *first_name, char *last_name) {     Person *person = malloc(sizeof(Person));     person->object.Print_Repr = Object_Print_Repr;     person->Repr = Person_Repr;     /* ... */     return person; }  /* Bird */ typedef struct Bird Bird; struct Bird {     Object object;     void (*Repr)(Bird*, char*);     /* ... */ };  Bird *New_Bird(char *name, Person *owner) {     Bird *bird = malloc(sizeof(Bird));     bird->object.Print_Repr = Object_Print_Repr;     bird->Repr = Bird_Repr;     /* ... */     return bird; }

Вот мы и реализовали паттерн «Шаблонный метод» на чистом Си. Не совсем честно, и не совсем надежно, но кое-как работает.

Тут два вопроса:

  1. Как быть, если функция Repr() не является вторым полем в структуре?

  2. Как быть, если хочется поддержки более чем одной функции?

Ответ не самый приятный, потому что портит всю красоту и чистоту базовой структуры Object, туда надо добавить адреса нужных нам функций. Получить их несложно, в stddef.h есть полезный макрос offsetof(<struct>, <field>). Работает он так:

struct A {     char c;     int i;     long l; }  offsetof(struct A, c) == 0; offsetof(struct A, i) == 4; offsetof(struct A, l) == 8;

С помощью этого макроса мы можем получить оффсеты всех нужных generic-функций, сохранить их в Object, и вызывать их оттуда из других методов. Красиво? А то!

Допустим, к функции Repr() мы захотели добавить функцию Str(), которая представит объект в виде строки, но без всякой дебажной шелухи, типа <Person first_name='Ivan' last_name='Ivanov'>, а просто сформирует строку Ivan Ivanov для вывода в каком-то интерфейсе. (Чувствуете веяние Python с его __repr__() и __str__()? Оно здесь не просто так, а сложно так.)

Соответственно, Object должен иметь соответствующую функцию Print_Str() для вывода результатов. А чтобы он цеплял правильную функцию, нужно внутри него прикопать все оффсеты.

Листинг будет больше остальных, с комментариями, но вы не бойтесь, мы все это скоро порефакторим.

#include <stdio.h> #include <stdlib.h> #include <stddef.h>  typedef struct Object Object; typedef struct Person Person; typedef struct Bird Bird;  struct Object {     size_t offset_repr;     void (*Print_Repr)(Object*);      size_t offset_str;     void (*Print_Str)(Object*); };  /*  Получить функцию по адресу object + offset_repr,  кастануть ее к void(*)(Object*, char*) и вызвать,  передав адрес текущего объекта.  */ void Object_Print_Repr(Object *object) {     void **p_repr_func = (void*) object + object->offset_repr;     void (*repr_func)(Object*, char*) = *p_repr_func;     char buf[80];     repr_func(object, buf);     printf("%s\n", buf); }  /*  То же самое, только теперь вместо offset_repr берем offset_str.  Сигнатура функции такая же, поэтому больше ничего интересного.  */ void Object_Print_Str(Object *object) {     void **p_str_func = (void*) object + object->offset_str;     void (*str_func)(Object*, char*) = *p_str_func;     char buf[80];     str_func(object, buf);     printf("%s\n", buf); }  /*  Обратите внимание на порядок полей в структуре,  теперь их можно группировать как угодно.  */ struct Person {     /* "Наследуемся" от Object */     Object object;      /* Собственно данные */     char *first_name;     char *last_name;      /* "Методы" */     void (*Repr)(Person*, char*);     void (*Str)(Person*, char*);     };  /* Person->Repr(...) */ void Person_Repr(Person *person, char *buf) {     sprintf(buf, "<Person: first_name='%s' last_name='%s'>",             person->first_name, person->last_name); }  /* Person->Str(...) */ void Person_Str(Person *person, char *buf) {     sprintf(buf, "%s %s", person->first_name, person->last_name); }  /*  Инициализация Person и вложенной структуры Object  */ Person *New_Person(char *first_name, char *last_name) {     /*      Собираем данные и функции самого Person.      */     Person *person = malloc(sizeof(Person));     person->first_name = first_name;     person->last_name = last_name;     person->Repr = Person_Repr;     person->Str = Person_Str;      /*      Оповещаем вложенный Object об адресах "дочерних"      функций, которые мы собираемся вызывать из самого Object      */     person->object.offset_repr = offsetof(Person, Repr);     person->object.offset_str = offsetof(Person, Str);      /* И наполняем его смыслом */     person->object.Print_Repr = Object_Print_Repr;     person->object.Print_Str = Object_Print_Str;          return person; }  /* Не забываем подчищать за собой */ void Del_Person(Person *person) {     free(person); }  /* Со структурой Bird все ровно так же, комментарии излишни. */ struct Bird {     Object object;      char *name;     Person *owner;      void (*Repr)(Bird*, char*);     void (*Str)(Bird*, char*); };  void Bird_Repr(Bird *bird, char* buf) {     char owner_repr[80];     bird->owner->Repr(bird->owner, owner_repr);     sprintf(buf, "<Bird: name='%s' owner=%s>",             bird->name, owner_repr); }  void Bird_Str(Bird *bird, char* buf) {     sprintf(buf, "%s", bird->name); }  Bird *New_Bird(char *name, Person *owner) {     Bird *bird = malloc(sizeof(Bird));     bird->name = name;     bird->owner = owner;     bird->Repr = Bird_Repr;     bird->Str = Bird_Str;      bird->object.offset_repr = offsetof(Bird, Repr);     bird->object.offset_str = offsetof(Bird, Str);      bird->object.Print_Repr = Object_Print_Repr;     bird->object.Print_Str = Object_Print_Str;      return bird; }  void Del_Bird(Bird *bird) {     free(bird); }  int main(void) {     Person *person = New_Person("Oleg", "Olegov");     Bird *bird = New_Bird("Kukushka", person);      /*      "Смотрим" на объект person как на Object      и вызываем функции с этим же объектом.      В принципе, никто не запрещает передать person      в функцию без дополнительного приведения типа:                ((Object*)person)->Print_Repr(person);       GCC это схавает, но выкинет warning.      */     ((Object*)person)->Print_Repr((Object*)person);     ((Object*)person)->Print_Str((Object*)person);          ((Object*)bird)->Print_Repr((Object*)bird);     ((Object*)bird)->Print_Str((Object*)bird);      Del_Bird(bird);     Del_Person(person); }

Выглядит прикольно, но бредовато. Во-первых, много boilerplate-кода в инициализаторах, а во-вторых, постоянный кастинг (Object*) просто кричит о протекающих абстракциях.

В принципе, последнюю проблему решить не так сложно. Достаточно добавить все Print_* функции в дочерние структуры и снабдить их указателями на те же самые функции из Object:

struct Person {     /* ... */     /* Ссылки на соответствующие функции Object */     void (*Print_Repr)(Person*);     void (*Print_Str)(Person*); };  Person *New_Person(char *first_name, char *last_name) {     /* ... */     person->object.Print_Repr = Object_Print_Repr;     person->object.Print_Str = Object_Print_Str;      /*      Вставляем те же самые функции в person,      приведя их к void (*)(Person *), чтобы компилятор      не ругался.      */     person->Print_Repr = (void (*)(Person *))Object_Print_Repr;     person->Print_Str = (void (*)(Person *))Object_Print_Str;          return person; }  /* Bird - то же самое */  int main(void) {     /* ... */     person->Print_Repr(person);     person->Print_Str(person);          bird->Print_Repr(bird);     bird->Print_Str(bird);      /* ... */ }

Теперь совсем красота, ООП во все щели! Дергаем метод person->Print_Repr(), который на самом деле person->object.Print_Repr(), который при вызове дергает person->Repr().

Но boilerplate-кода все еще неприлично много. Каждый раз всю нашу ООП-машинерию нужно описывать в инициализаторах, и не дай боже что-то пропустить — SEGFAULT не дремлет!

Представляем — object.h:

#pragma once  #include <stddef.h>  /*  Макрос, который встраивает нужные поля объекта.  */ #define OBJECT(T) \     Object object; \     void (*Repr)(T*, char*); \     void (*Str)(T*, char*); \     void (*Print_Repr)(T*); \     void (*Print_Str)(T*);  /*  Инициализатор объекта, подсовывающий все  нужные функции и оффсеты  */ #define INIT_OBJECT(x, T) \     x->object.Print_Repr = Object_Print_Repr; \     x->object._offset_Repr = offsetof(T, Repr); \     x->object.Print_Str = Object_Print_Str; \     x->object._offset_Str = offsetof(T, Str); \     x->Print_Repr = (void (*) (T*)) Object_Print_Repr; \     x->Print_Str = (void (*) (T*)) Object_Print_Str; \     x->Repr = T ## _Repr; \     x->Str = T ## _Str  /* Макрос, возвращающий указатель на функцию по ее названию */ #define OBJECT_FUNC(x, F) *(void **)((void*) x + x->_offset_ ## F)  typedef struct Object Object;  typedef void *(Repr)(Object *, char*); typedef void *(Str)(Object *, char*);  /* Наши старые знакомые */ struct Object {     size_t _offset_Repr;     void (*Print_Repr)(Object*);      size_t _offset_Str;     void (*Print_Str)(Object*); };  void Object_Print_Repr(Object *object) {     Repr *repr_func = OBJECT_FUNC(object, Repr);     char buf[80];     repr_func(object, buf);     printf("%s\n", buf); }  void Object_Print_Str(Object *object) {     Str *str_func = OBJECT_FUNC(object, Str);     char buf[80];     str_func(object, buf);     printf("%s\n", buf); }

И вот как эти макросы сокращают объем финального кода:

typedef struct Person Person; typedef struct Bird Bird;  struct Person {     /*      Это не обычная структура, а наследник      абстракции по имени Object      */     OBJECT(Person)     char *first_name;     char *last_name; };  void Person_Repr(Person *person, char *buf) {     sprintf(buf, "<Person: first_name='%s' last_name='%s'>",             person->first_name, person->last_name); }  void Person_Str(Person *person, char *buf) {     sprintf(buf, "%s %s", person->first_name, person->last_name); }  Person *New_Person(char *first_name, char *last_name) {     Person *person = malloc(sizeof(Person));      /*      INIT_OBJECT() цепляет все нужные функции,      включая Person_Repr и Person_Str, и подсовывает      их в соответствующие поля структуры      */     INIT_OBJECT(person, Person);      person->first_name = first_name;     person->last_name = last_name;      return person; }  /*  Извините, но реализация garbage collector на Си -  тема отдельного выпуска  */ void Del_Person(Person *person) {     free(person); }  /* Bird снова ничем не отличается от Person */ struct Bird {     OBJECT(Bird)     char *name;     Person *owner; };  void Bird_Repr(Bird *bird, char* buf) {     char owner_repr[80];     bird->owner->Repr(bird->owner, owner_repr);     sprintf(buf, "<Bird: name='%s' owner=%s>",             bird->name, owner_repr); }  void Bird_Str(Bird *bird, char* buf) {     sprintf(buf, "%s", bird->name); }  Bird *New_Bird(char *name, Person *owner) {     Bird *bird = malloc(sizeof(Bird));     INIT_OBJECT(bird, Bird);      bird->name = name;     bird->owner = owner;     return bird; }  void Del_Bird(Bird *bird) {     free(bird); }  int main(void) {     Person *person = New_Person("Oleg", "Olegov");     Bird *bird = New_Bird("Kukushka", person);      /*      Вызываем разные экземпляры "родительских" функций      Print_Repr и Print_Str      */     person->Print_Repr(person);     bird->Print_Repr(bird);      person->Print_Str(person);     bird->Print_Str(bird);      Del_Bird(bird);     Del_Person(person); }

Самое прелестное в этих макросах — это обеспечение compile-time проверок. Допустим, мы решили добавить новую структуру, «наследовали» ее от Object, но обязательных методов Repr и Str не объявили:

typedef struct Fruit Fruit;  struct Fruit {     OBJECT(Fruit)     char *name; };  Fruit *New_Fruit(char *name) {     Fruit *fruit = malloc(sizeof(Fruit));     INIT_OBJECT(fruit, Fruit);      fruit->name = name;     return fruit; }  void Del_Fruit(Fruit *fruit) {     free(fruit); }

И тогда нам незамедлительно прилетает от компилятора:

c_inheritance.c: In function ‘New_Fruit’: c_inheritance.c:77:24: error: ‘Fruit_Repr’ undeclared (first use in this function)    77 |     INIT_OBJECT(fruit, Fruit);       |                        ^~~~~ <...> c_inheritance.c:77:24: error: ‘Fruit_Str’ undeclared (first use in this function)    77 |     INIT_OBJECT(fruit, Fruit);       |                        ^~~~~

Очень удобно!

А в чем же здесь стреляние по ногам? — спросите вы. Раз уж все так клево, почему бы не применить это в промышленной разработке?

Во-первых, в команде вас будут считать наркоманом.

Во-вторых, даже если не будут, то скорость программы снизится. А Си используют как раз для того, чтобы эту скорость приобрести, и часто за нее приходится платить дублированием кода и избеганием абстракций. И несмотря на то, что компиляторы нынче супер-оптимизирующие, ассемблерный выхлоп из «ООП»-кода и кода с парой простых функций Person_Print() и Bird_Print() даже с -O3 будет различаться в полтора-два раза (не в пользу первого).

Посему данная статья носит исключительно информационный характер, а никак не рекомендательный.


ссылка на оригинал статьи https://habr.com/ru/post/673270/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *