TL;DRhttps://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 будет различаться в полтора-два раза (не в пользу первого).

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

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)


  1. CloseToAlgotrading
    24.06.2022 15:23

    Хохо, эко Вы заморочились, но в целом очень прикольно.

    Интересно, а в каких проектах промышленной разработки в принципе такой код может быть разрешен? Есть у меня подозрения, что почти нигде.


    1. pomidoroshev Автор
      24.06.2022 15:28
      +5

      чтобы прямо ровно такой код - не знаю, но я вдохновлялся исходниками CPython


    1. includedlibrary
      24.06.2022 15:30

      gtk тоже на си написан и там есть такое же ООП


      1. CloseToAlgotrading
        24.06.2022 15:39

        И то верно, cpython, gtk... для pc проектов вполне возможно. Хотя наверное я очень критичен с некоторыми изменениями, возможно наверное и в эмбедед это все отлично вписать.


        1. includedlibrary
          24.06.2022 15:48
          +3

          Я для эмбед на си ничего не писал. Но когда я писал на си для десктопа мне не сильно ООП не хватало, да и раст прекрасно без него обходится. Поэтому смысла особого не вижу такой код писать


          1. ZyXI
            26.06.2022 20:38

            В rust есть trait’ы. В C мне вместо них не раз приходилось использовать либо функции, генерируемые макросами, либо .c.h файлы. Не знаю, как называется последняя техника, но уверен, что не я первый её придумал: идея в том, что у нас есть файл frob.c.h вида


            // FROB_ACTION has default value.
            #ifndef FROB_ACTION
            # define _FROB_ACTION_DEFINED
            # define FROB_ACTION(a, b) (a) += (b)
            #endif
            
            #define _FROB_FUNCNAME(suffix) FROB_PREFIX##suffix
            
            static FROB_RETURN_TYPE _FROB_FUNCNAME(_frobnicate)(int arg1)
            {
                FROB_RETURN_TYPE ret = 1;
                FROB_ACTION(ret, 1);
                return ret;
            }
            
            #undef _FROB_FUNCNAME
            
            #ifdef _FROB_ACTION_DEFINED
            # undef _FROB_ACTION_DEFINED
            # undef FROB_ACTION
            #endif

            и он используется так:


            #define FROB_PREFIX froba
            #define FROB_RETURN_TYPE int
            #include "frob.c.h"
            #undef FROB_RETURN_TYPE
            #undef FROB_PREFIX
            
            #define FROB_PREFIX frobs
            #define FROB_RETURN_TYPE int
            #define FROB_ACTION(a, b) (a) <<= (b)
            #include "frob.c.h"
            #undef FROB_ACTION
            #undef FROB_RETURN_TYPE
            #undef FROB_PREFIX
            
            int main(const int argc, const char *const *const argv)
            {
                if (froba_frobnicate(argc) > 0) {
                    return frobs_frobnicate(argc);
                } else {
                    return 0;
                }
            }

            (#undef тут везде только чтобы не засорять пространство имён).


            Такая вариация на тему generic’ов не слишком удобна, но она имеет несколько важных преимуществ перед определением функций в макросах:


            1. В отладчике функции теперь не в одну строку и вы можете нормально ставить точки останова.
            2. Подсветка синтаксиса работает лучше.
            3. Не нужно помнить про \ в конце строки.
            4. Можно сделать аргументы по‐умолчанию.

            Из недостатков в первую очередь только бо́льший размер кода. Техника для случаев, когда вы хотите что‐то вроде HashMap<K, V> со всеми его функциями, но в C — т.е. когда кода достаточно много, чтобы вас волновало удобство работы с ним.


        1. kenshi0771
          26.06.2022 19:44

          rt thread можете посмотреть. По моему реализация этой rtos для эмбедед очень напоминает методы, описанные в статье.


    1. UnknownUser
      24.06.2022 17:17

      В далеком 2008 году я использовал похожий подход для программирования интерфейса в одном промышленном приборе.

      Компилятор позволял писать только на Си, а все подходы организации подобного в процедурном стиле, которые я видел, были ещё ужаснее, на мой скромный взгляд.

      При должной самодисциплине вполне себе работает, но мне повезло что команда состояла из одного меня и (потом) одного студента :-)


    1. HenadziMatuts
      24.06.2022 23:57

      StrongSwan VPN - https://docs.strongswan.org/docs/5.9/devs/objectOrientedC.html, там ещё сильнее заморочились в плане синтаксического сахара на макросах. При этом общую идею авторы взяли из xine. Вполне живая и рабочая схема на самом деле.


  1. VadimZud
    24.06.2022 15:56
    +5

    Есть книжка "Object-Oriented Programming With ANSI-C" автора Axel-Tobias Schreiner на эту тему


    1. Sovietmade
      25.06.2022 21:44
      +1

      Сохраню коммент, вдруг, не дай бог, пригодится


  1. 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 или до конца форматной строки.

    Плюсы:

    • можно печатать структуру, задавая ее в форматной строке

    • можно печатать сразу несколько структур (нет какого-то буфера, который затрется)

    Минусы:

    • копирование форматной строки и аллокация


    1. win32asm
      25.06.2022 00:47

      Не совсем портабельно, но

      https://www.gnu.org/software/libc/manual/html_node/Printf-Extension-Example.html


  1. Kelbon
    24.06.2022 20:24
    +4

    А можно просто использовать С++...


    1. Spym
      24.06.2022 20:27
      +5

      Использовать какой-то другой язык вместо си? Да ну, бред какой-то.


      1. Kelbon
        24.06.2022 21:02
        +2

        Использовать какой-то другой язык вместо С++? Да ну, бред какой-то.


        1. Spym
          24.06.2022 21:04
          +1

          Мы заигрались.


  1. 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

    Я постарался описать в комментариях другие варианты исполнения.

    Основная мысль в том, что тут не нужны ни макросы, ни сложные системы каста. Достаточно лишь использовать то свойство языка, что первый элемент структуры по адресу равен своему родителю.


    1. OlegZH
      25.06.2022 13:49

      Подзабыл Си. ;-( Но есть возможность вспомнить!

      Вы написали довольно понятный код. Не могли бы Вы пояснить пару моментов?

      1. Что такое PRIu32?

      2. В вашей реализации получается, что разные экземпляры одного и того же объекта (структуры) всё своё носят с собой (например, указатели методы). Не так ли?

      3. Как работает функция realloc? Она просто пытается расширить область памяти под объект без потери ранее построенных объектов?


      1. staticmain
        25.06.2022 14:20

        1. Это макросы из стандартного заголовка 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) функции.

        2. Да, так и есть. Это позволяет использовать именно тот принтер, который установлен для данного экземпляра (так как передав, например square в функцию, принимающую figure вы всё еще сможете использовать именно тот принтер, который выводит square). Точно так же поступают реализации классов, основанные на vtable в таких языках, как например С++. Такая таблица с указателями на функции имеется у каждого экземпляра класса для ускорения поиска необходимого метода.

        3. В зависимости от реализации в libc она работает "под капотом" по разному, но в большинстве случаев принцип следующий:

          • Читается информация о длине переданного выделенного блока памяти

          • Если справа от границы выделенного блока достаточно места (суммарно столько, сколько передано в функцию realloc), то меняется информация о длине выделенного блока (тут возможны варианты сколько именно, например в некоторых реализациях она может удваиваться), память помечается как используемая, а в качестве результата передается исходный указатель.

          • Если справа от границы выделенного блока недостаточно места (дефрагментация памяти), то среди свободных ячеек ищется интервал необходимой длины (в зависимости от алгоритма выделения), туда копируется вся использованная в старом интервале память, а затем возвращается новый указатель. Старый указатель при этом освобождается.

          • Если интервала необходимой длины не найдено (нет памяти и выключен overcommit), то в качестве результата будет возвращен NULL, а старый указатель не будет освобожден. В коде выше эта проверка умышленно пропущена (см. комментарий в коде) для наглядности использования.


  1. yatagarasu
    25.06.2022 03:09
    +3

    До таблицы виртуальных функций так и не дошли (да и вообще пошли в другом направлении) - т.е. чем больше функций тем больше объекты.


  1. OlegZH
    25.06.2022 13:53

    А какой компилятор Вы использовали? Хотелось бы проверит по шагам Ваше решение.


    1. pomidoroshev Автор
      25.06.2022 14:08

      gcc и clang.

      мне кажется, и msvc должен проканать, я старался не завязываться на особенностях компиляторов. но если вдруг не проканает, напишите


      1. includedlibrary
        26.06.2022 18:23

        msvc для си лучше не использовать, так как microsoft его не обновляют. Я пытался скомпилировать код на c11 и обнаружил, что он поддерживает то ли c99, то ли ansi c89.


  1. Helltraitor
    25.06.2022 14:08

    Как выстрелить себе в ногу на языке C

    Использует буфферы... Я не спец, но может стоит использовать структуру на куче (в C ведь есть нормальный string)? Или хотя бы проверять длину строки, которая в буфер лезет?


    1. pomidoroshev Автор
      25.06.2022 14:10

      да, лучше заморочиться с двойным вызовом snprintf. это выходит за пределы скоупа текущей задачи, поэтому я решил по-простому.


      1. pomidoroshev Автор
        26.06.2022 19:44

        дописал UPD


  1. Alexey_Sharapov
    26.06.2022 09:22

    А не проще generic юзать? Стандартный _Generic уже 11 лет с нами, а в gcc и clang generic через built-inы и того раньше появились.


  1. xabar
    27.06.2022 09:51

    Ой вей. Именно так и появился GTK.

    Нужно ООП - пиши на плюсах.

    Хочешь поупражняться в сверхуме - пиши на Аде.

    На Си программа должна быть "палка-веревка". По другому - да, вы наркоман.