Иногда нет-нет да и хочется что-нибудь абстрагировать и обобщить в коде на Си. К примеру, хочешь ты принтануть содержимое структуры несколько раз, пишешь везде, как дурак, 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);
Комментарии (29)
VadimZud
24.06.2022 15:56+5Есть книжка "Object-Oriented Programming With ANSI-C" автора Axel-Tobias Schreiner на эту тему
Alex_ME
24.06.2022 18:11+4К примеру, хочешь ты принтануть содержимое структуры несколько раз
Их есть у меня. Без ООП, абстракций, а исключительно печать структур, при этом не какой-нибудь там отдельной функцией в буфер, а возможность печатать структуру как один из спецификаторов форматной строки: https://github.com/Garrus007/advanced-fprintf
Вот что получаем:
struct foo foo = { .a=1, .b=2 }; struct bar bar = { .a=3.14, .b=10, .c="hello world" }; aprintf("Print: int: %d, foo: %Y, str: '%s', bar: %Y, bad one: %Y. The end!\n", 123, FORMAT_FOO(&foo), "some string", FORMAT_BAR(&bar), FORMAT_FOO(NULL));
Подготовка за кулисами
Ну, тут придется немного пописать, чтобы предоставить структуре возможность печататься...
struct foo { int a; int b; }; // Function to print "struct foo" to the FILE* void format_foo(FILE* f, void* data) { if (data == NULL) { fprintf(f, "foo(nill)"); return; } struct foo* foo = (struct foo*)data; fprintf(f, "foo{a=%d, b=%d}", foo->a, foo->b); } #define FORMAT_FOO(ptr)format_foo, (ptr)
Под капотом
А вот тут колдунство. Согласно стандарту, семейство *printf-функций не трогает "лишние" аргументы, после того, как форматная строка закончилась.
afprintf - это враппер над fprintf, который находит специальный спецификатор формата %Y, разделяет форматную строку по границе этого спецификатора, скармилвает подстроку обычному fprintf, потом печатает кастомную структуру, используая переданный указатель на функцию печати, а затем продолжает печатать обычным fprintf до следующего %Y или до конца форматной строки.
Плюсы:
можно печатать структуру, задавая ее в форматной строке
можно печатать сразу несколько структур (нет какого-то буфера, который затрется)
Минусы:
копирование форматной строки и аллокация
win32asm
25.06.2022 00:47Не совсем портабельно, но
https://www.gnu.org/software/libc/manual/html_node/Printf-Extension-Example.html
staticmain
24.06.2022 22:18Я искренне не понимаю, зачем все присутствующие использовали макросы и\или двойные указатели.
То, что показал автор - это просто инкапсуляция средствами языка Си, которая может быть выполнена как через инкапусляцию структур, так и через union.
Например: https://onlinegdb.com/EtzkwAwAD
Код с oblinegdb
#include <stdio.h> #include <stdint.h> #include <inttypes.h> #include <stdlib.h> /** ООП через инкапсуляцию **/ /* Проверка на выделение памяти опущена умышленно */ /* Обратите внимание, что вы можете написать функции печати и ИНАЧЕ, * Имея функцию печати для *каждого* объекта не перетирая старую. * Это позволит перед выводом новых полей вывести старые, вызвав функцию * глубиной "ниже" * Например: * ~~~ * struct my_figure { * struct my_object object; * object_printer_t printer; // = _printer_figure * } * void _printer_figure(void * object) { * struct my_figure * figure = object; * // Вызываем принтер объекта, в котором печатаем имя и, например ptr * figure->object.printer(figure->object); * // Непосредственно новые данные фигуры * printf( * "Данные фигуры: %" PRIu32 ":%" PRIu32 "\n", * figure->coords.x, * figure->coords.y * ); * } * ~~~ * В объекте следующего уровня не нужно вызывать `figure->object.printer` * Достаточно вызвать принтер непосредственного родителя: * ~~~ * struct my_square * square = object; * square->figure.printer(square->figure); * <...> * ~~~ */ /* Также можно применить union, для того, чтобы при доступе к полям структуры * не перечислять всех родителей чтобы добраться до нужного поля: * ~~~ * union my_square { * struct my_object object; * struct my_figure figure; * struct { * uint32_t width; * } square; * } * * my_square->square.width; * my_square->figure.points; * my_square->object.name; * ~~~ * Это потребует перечислить все подлежащие объекты в объявлении (если, конечно, * вы не хотите скрыть часть из них) */ /* Также вместо приведения типа в каждом месте можно использовать функцию, * которая скроет от пользователя каст, что позволит иметь более чистый код * плюс позволит достаточно быстро изменить структуру наследования: * ~~~ * struct my_figure * _figure_get(void * ptr) { * return (struct my_figure *)ptr; * } * * struct my_figure * figure = _figure_get(square); * ~~~ * Вы можете добавить защиту от дурака при помощи typeof самостоятельно, * позволяя кастить только дозволенные объекты при условии, что тип объекта * известен compile-time */ /*! \brief Функция печати данных объекта. */ typedef void (* object_printer_t)(void * object); /* ## Первый уровень */ /*! \brief Объект-родитель */ struct my_object { object_printer_t printer; /**< Функция печати данных объекта */ const char * name; /**< Имя объекта */ }; /*! \brief Функция печати объекта. Обычно замещается другой * \param[in] object Объект `struct my_object *` */ static void _printer_object(void * object) { struct my_object * my_object = object; printf("Object: %s\n", my_object->name); } /*! \brief Конструктор объекта * \param[in] name Имя объекта * \return Объект */ static struct my_object * _create_object(const char * name) { struct my_object * object = malloc(sizeof(struct my_object)); object->printer = _printer_object; object->name = name; return object; } /*! \brief Деструктор объекта * \param[in] object Объект */ static void _destroy_object(struct my_object * object) { free(object); } /* ## Второй уровень */ /*! \brief Пример первого уровня - Фигура */ struct my_figure { struct my_object object; /**< Родительский объект */ uint32_t points; /**< Количество углов */ struct { /**< Координаты LT-угла */ uint32_t x; /**< Горизонтальная */ uint32_t y; /**< Вертикальная */ } coords; }; /*! \brief Функция печати фигуры. * \param[in] object Объект `struct my_figure *` */ static void _printer_figure(void * object) { struct my_object * my_object = object; struct my_figure * my_figure = object; printf( "Figure \"%s\": %" PRIu32 ":%" PRIu32 "\n", my_object->name, my_figure->coords.x, my_figure->coords.y ); } /*! \brief Конструктор фигуры * \param[in] name Имя фигуры * \param[in] points Количество углов фигуры * \param[in] x X-координата фигуры * \param[in] y Y-координата фигуры * \return Фигура */ static struct my_figure * _create_figure( const char * name, uint32_t points, uint32_t x, uint32_t y ) { // Создаем объект на 1 уровень ниже struct my_object * object = _create_object(name); object->printer = _printer_figure; // Создаем свойства текущего уровня struct my_figure * figure = realloc(object, sizeof(struct my_figure)); figure->points = points; figure->coords.x = x; figure->coords.y = y; return figure; } /*! \brief Деструктор фигура * \param[in] object Фигура */ static void _destroy_figure(struct my_figure * figure) { // Здесь мы должны вызвать деструкторы полей НАШЕГО объекта. // У нас их нет. Иначе мы могли бы вызвать например free(figure->string); // Вызываем деструктор родителя _destroy_object((struct my_object *)figure); } /* ## Третий уровень */ /*! \brief Пример третьего уровня - Квадрат */ struct my_square { struct my_figure figure; /**< Родительский объект */ uint32_t width; /**< Размер грани куба */ }; /*! \brief Функция печати фигуры. * \param[in] object Объект `struct my_figure *` */ static void _printer_square(void * object) { struct my_object * my_object = object; struct my_figure * my_figure = object; struct my_square * my_square = object; printf( "Square \"%s\": %" PRIu32 ":%" PRIu32 ", with a side of %" PRIu32 "\n", my_object->name, my_figure->coords.x, my_figure->coords.y, my_square->width ); } /*! \brief Конструктор квадрата * \param[in] name Имя квадрата * \param[in] width Ширина квадрата * \param[in] x X-координата квадрата * \param[in] y Y-координата квадрата * \return Квадрат */ static struct my_square * _create_square( const char * name, uint32_t width, uint32_t x, uint32_t y ) { static const uint32_t square_points = 4; // Создаем объект на 1 уровень ниже struct my_figure * figure = _create_figure(name, square_points, x, y); // Нам всё еще нужен объект для переписываения принтера. // См. одно из примечаний для того чтобы сделать иначе. struct my_object * object = (struct my_object *)figure; object->printer = _printer_square; // Создаем свойства текущего уровня struct my_square * square = realloc(figure, sizeof(struct my_square)); square->width = width; return square; } /*! \brief Деструктор квадрата * \param[in] object Объект */ static void _destroy_square(struct my_square * square) { // Здесь мы должны вызвать деструкторы полей НАШЕГО объекта. // У нас их нет. Иначе мы могли бы вызвать например free(square->alloced); // Вызываем деструктор родителя // См. одно из примечаний для того, чтобы не использовать каст _destroy_figure((struct my_figure *)square); } int main() { struct my_object * object = _create_object("Just an object"); struct my_figure * figure = _create_figure("Figure", 5, 21, 34); struct my_square * square = _create_square("Square", 50, 1, 2); // Вы можете не использовать всю цепочку, если будете использовать union // как в одном из примечаний. object->printer(object); figure->object.printer(figure); square->figure.object.printer(square); // Мы также можем использовать принтер в функции, которая принимает // более неглубокую версию объекта: struct my_object * casted_object = (struct my_object *)square; casted_object->printer(casted_object); // Мы всё равно получим вывод исходного объекта // Ну и также мы можем использовать принтер более низкого уроня на более // высокий объект: figure->object.printer(square); _destroy_square(square); _destroy_figure(figure); _destroy_object(object); return 0; }
Object: Just an object Figure "Figure": 21:34 Square "Square": 1:2, with a side of 50 Square "Square": 1:2, with a side of 50 Figure "Square": 1:2
Я постарался описать в комментариях другие варианты исполнения.
Основная мысль в том, что тут не нужны ни макросы, ни сложные системы каста. Достаточно лишь использовать то свойство языка, что первый элемент структуры по адресу равен своему родителю.
OlegZH
25.06.2022 13:49Подзабыл Си. ;-( Но есть возможность вспомнить!
Вы написали довольно понятный код. Не могли бы Вы пояснить пару моментов?
Что такое PRIu32?
В вашей реализации получается, что разные экземпляры одного и того же объекта (структуры) всё своё носят с собой (например, указатели методы). Не так ли?
Как работает функция realloc? Она просто пытается расширить область памяти под объект без потери ранее построенных объектов?
staticmain
25.06.2022 14:20Это макросы из стандартного заголовка inttypes.h. Они содержат модификаторы форматирования для printf-like функций, которые нужны для вывода типа определенной длины. Это необходимо, когда вы пишете кроссплатформенный код, так как под разными платформами вам могут понадобиться разные модификаторы. Например есть различия в объявлении uint64_t под gcc и mingw - long unsigned int (модификатор ul) и long long unsigned int (ull). Используя PRIu64 вы сможете обойтись без ifdef директив при использовании каждой printf-like (printf, sprintf, sscanf, etc) функции.
Да, так и есть. Это позволяет использовать именно тот принтер, который установлен для данного экземпляра (так как передав, например square в функцию, принимающую figure вы всё еще сможете использовать именно тот принтер, который выводит square). Точно так же поступают реализации классов, основанные на vtable в таких языках, как например С++. Такая таблица с указателями на функции имеется у каждого экземпляра класса для ускорения поиска необходимого метода.
-
В зависимости от реализации в libc она работает "под капотом" по разному, но в большинстве случаев принцип следующий:
Читается информация о длине переданного выделенного блока памяти
Если справа от границы выделенного блока достаточно места (суммарно столько, сколько передано в функцию realloc), то меняется информация о длине выделенного блока (тут возможны варианты сколько именно, например в некоторых реализациях она может удваиваться), память помечается как используемая, а в качестве результата передается исходный указатель.
Если справа от границы выделенного блока недостаточно места (дефрагментация памяти), то среди свободных ячеек ищется интервал необходимой длины (в зависимости от алгоритма выделения), туда копируется вся использованная в старом интервале память, а затем возвращается новый указатель. Старый указатель при этом освобождается.
Если интервала необходимой длины не найдено (нет памяти и выключен overcommit), то в качестве результата будет возвращен NULL, а старый указатель не будет освобожден. В коде выше эта проверка умышленно пропущена (см. комментарий в коде) для наглядности использования.
yatagarasu
25.06.2022 03:09+3До таблицы виртуальных функций так и не дошли (да и вообще пошли в другом направлении) - т.е. чем больше функций тем больше объекты.
OlegZH
25.06.2022 13:53А какой компилятор Вы использовали? Хотелось бы проверит по шагам Ваше решение.
pomidoroshev Автор
25.06.2022 14:08gcc и clang.
мне кажется, и msvc должен проканать, я старался не завязываться на особенностях компиляторов. но если вдруг не проканает, напишите
includedlibrary
26.06.2022 18:23msvc для си лучше не использовать, так как microsoft его не обновляют. Я пытался скомпилировать код на c11 и обнаружил, что он поддерживает то ли c99, то ли ansi c89.
Helltraitor
25.06.2022 14:08Как выстрелить себе в ногу на языке C
Использует буфферы... Я не спец, но может стоит использовать структуру на куче (в C ведь есть нормальный string)? Или хотя бы проверять длину строки, которая в буфер лезет?
pomidoroshev Автор
25.06.2022 14:10да, лучше заморочиться с двойным вызовом snprintf. это выходит за пределы скоупа текущей задачи, поэтому я решил по-простому.
Alexey_Sharapov
26.06.2022 09:22А не проще generic юзать? Стандартный _Generic уже 11 лет с нами, а в gcc и clang generic через built-inы и того раньше появились.
xabar
27.06.2022 09:51Ой вей. Именно так и появился GTK.
Нужно ООП - пиши на плюсах.
Хочешь поупражняться в сверхуме - пиши на Аде.
На Си программа должна быть "палка-веревка". По другому - да, вы наркоман.
CloseToAlgotrading
Хохо, эко Вы заморочились, но в целом очень прикольно.
Интересно, а в каких проектах промышленной разработки в принципе такой код может быть разрешен? Есть у меня подозрения, что почти нигде.
pomidoroshev Автор
чтобы прямо ровно такой код - не знаю, но я вдохновлялся исходниками CPython
includedlibrary
gtk тоже на си написан и там есть такое же ООП
CloseToAlgotrading
И то верно, cpython, gtk... для pc проектов вполне возможно. Хотя наверное я очень критичен с некоторыми изменениями, возможно наверное и в эмбедед это все отлично вписать.
includedlibrary
Я для эмбед на си ничего не писал. Но когда я писал на си для десктопа мне не сильно ООП не хватало, да и раст прекрасно без него обходится. Поэтому смысла особого не вижу такой код писать
ZyXI
В rust есть trait’ы. В C мне вместо них не раз приходилось использовать либо функции, генерируемые макросами, либо
.c.h
файлы. Не знаю, как называется последняя техника, но уверен, что не я первый её придумал: идея в том, что у нас есть файлfrob.c.h
видаи он используется так:
(
#undef
тут везде только чтобы не засорять пространство имён).Такая вариация на тему generic’ов не слишком удобна, но она имеет несколько важных преимуществ перед определением функций в макросах:
\
в конце строки.Из недостатков в первую очередь только бо́льший размер кода. Техника для случаев, когда вы хотите что‐то вроде
HashMap<K, V>
со всеми его функциями, но в C — т.е. когда кода достаточно много, чтобы вас волновало удобство работы с ним.kenshi0771
rt thread можете посмотреть. По моему реализация этой rtos для эмбедед очень напоминает методы, описанные в статье.
UnknownUser
В далеком 2008 году я использовал похожий подход для программирования интерфейса в одном промышленном приборе.
Компилятор позволял писать только на Си, а все подходы организации подобного в процедурном стиле, которые я видел, были ещё ужаснее, на мой скромный взгляд.
При должной самодисциплине вполне себе работает, но мне повезло что команда состояла из одного меня и (потом) одного студента :-)
HenadziMatuts
StrongSwan VPN - https://docs.strongswan.org/docs/5.9/devs/objectOrientedC.html, там ещё сильнее заморочились в плане синтаксического сахара на макросах. При этом общую идею авторы взяли из xine. Вполне живая и рабочая схема на самом деле.