Иногда нет-нет да и хочется что-нибудь абстрагировать и обобщить в коде на Си. К примеру, хочешь ты принтануть содержимое структуры несколько раз, пишешь везде, как дурак, 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; }
Вот мы и реализовали паттерн "Шаблонный метод" на чистом Си. Не совсем честно, и не совсем надежно, но кое-как работает.
Тут два вопроса:
Как быть, если функция
Repr()не является вторым полем в структуре?Как быть, если хочется поддержки более чем одной функции?
Ответ не самый приятный, потому что портит всю красоту и чистоту базовой структуры 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 будет различаться в полтора-два раза (не в пользу первого).
Посему данная статья носит исключительно информационный характер, а никак не рекомендательный.
UPD Читатели справедливо заметили, что небезопасно использовать буфер фиксированного размера (char buf[80]), который я взял для упрощения кода. В реальной жизни, конечно, стоит выделять буфер по размеру финальной строки:
size_t size = snprintf(NULL, 0, "%s ...", foo, ...); char *buf = malloc(size + 1); if (buf == NULL) { return 1; } sprintf(buf, "%s ...", foo, ...); /* ... */ free(buf);
