Добавляем немного виртуальности в C

    Мне тут пришлось вспомнить, как же все таки надо писать на С, хотя работаю С++ программистом. И мне так не хватало классов и методов, что я стал думать, как можно приблизить С к С++. Зачем? Просто так, мозги размять.
    Основное пожелание было следующим: хочется, чтобы в С работал примерно такой код:

    void print_name( Iface* ptr )
    {
        ptr->print_name();
    }
    
    void main()
    {
        A a;
        B b;
    
        print_name( &a );    // выдаст "This is A object"
        print_name( &b );    // выдаст "This is B object"
    }
    


    По сути, A и В наследуют один и тот же (а может и несколько) интерфейс(ов). При этом можно на этих объектах вызывать виртуальные методы, действующие в зависимости от того, какой указатель был подан.

    Кому интересно, что получилось в итоге (а в каком-то виде задача была решена) и кому интересно, как примерно в С++ реализуются виртуальные методы, прошу под кат.




    Что есть готового


    Естественно, я не один такой умник. Методы на структурах реализуются через указатели на функции. Вот одно из решений, от которого мы будем отталкиваться.

    Какие есть недостатки у такого решения?

    • необходимость явного обращения к таблице методов структуры
    • сложность (нереальность?) реализовать множественное «наследование» интерфейсов


    Как будем решать задачу?


    Вместо отдельной структуры, являющейся таблицей методов, положим в нашу структуру явно указатели на функции. Причем положим в самое начало для простоты.
    Если нужно сделать множественное «наследование», будем класть последовательно сначала методы одного интерфейса, затем методы второго и т.д.

    В функции, принимающие указатели на интерфейс, будем передавать указатель на первый метод интерфейса в структуре. Так как этот метод будет иметь специфичную для структуры реализацию, мы можем заложиться на смещение этого адреса от начала адреса структуры в целом.

    Недостаток здесь очевиден: надо учитывать смещения в структуре. Следовательно, нужно либо упаковывать структуру, либо каким-то образом узнавать смещение. Я сделал проще: забил и считал, что выравнивания по размеру указателя будет достаточно.

    Далее будет приведена реализация задуманного при помощи precompiled headers и VS2010 в режиме С компилятора.

    Поехали!


    Интерфейсы

    Итак, введем два интерфейса.
    Первый, base_iface, позволяет вызывать метод, возвращающий строку с описанием объекта.
    Второй, sizible_iface, позволяет узнать размер объекта.
    Примеры надуманные, но нам сойдет.
    base_iface.h
    #pragma once
    
    typedef struct base_iface* base_iface_ptr;
    typedef const char* (*get_name_func_ptr)( base_iface_ptr );
    
    struct base_iface
    {
        get_name_func_ptr get_name;
    };
    
    typedef struct base_iface base_iface;
    

    sizible_iface.h
    #pragma once
    
    typedef struct sizible_iface* sizible_iface_ptr;
    typedef unsigned int (*get_size_func_ptr)( sizible_iface_ptr );
    
    struct sizible_iface
    {
        get_size_func_ptr get_size;
    };
    
    typedef struct sizible_iface sizible_iface;
    


    Классы

    Сделаем две структуры, point_t и d3_point_t, представляющие собой точку в двумерном и трехмерном пространстве соответственно. Каждая структура содержит координаты точки, а также три указателя на функции: «получить имя объекта», «получить размер объекта» и «напечатать координаты точки».

    Первый указатель — виртуальный метод, «наследование» интерфейса base_iface.
    Второй указатель — виртуальный метод, «наследование» интерфейса sizible_iface.
    Третий указатель — обычный метод, не виртуальный.

    Заголовочные файлы приведены ниже:
    point.h
    #pragma once
    
    typedef struct point_t* point_ptr;
    typedef void (*point_print_coordinates_func)( point_ptr );
    
    struct point_t  // наследует base_iface и sizible_iface
    {
        // "виртуальные методы"
        get_name_func_ptr get_name;
        get_size_func_ptr get_size;
    
        // остальные "методы"
        point_print_coordinates_func print_coordinates;
    
        // координаты
        int x;
        int y;
    };
    
    typedef struct point_t point_t;
    
    // конструктор
    point_t point_init( int x, int y );
    
    // ручное приведение типов от point_ptr к base_iface_ptr
    base_iface_ptr point_to_base_iface( point_ptr ptr );
    
    // ручное приведение типов от point_ptr к sizible_iface_ptr
    sizible_iface_ptr point_to_sizible_iface( point_ptr ptr );
    

    3d_point.h
    #pragma once
    
    typedef struct d3_point_t* d3_point_ptr;
    typedef void (*d3_point_print_coordinates_func)( d3_point_ptr );
    
    struct d3_point_t // наследует base_iface и sizible_iface
    {
        // обычные "методы"
        d3_point_print_coordinates_func print_coordinates;
    
        // "виртуальные методы"
        get_name_func_ptr get_name;
        get_size_func_ptr get_size;
    
        // координаты
        int x;
        int y;
        int z;
    };
    
    typedef struct d3_point_t d3_point_t;
    
    // конструктор
    d3_point_t d3_point_init( int x, int y, int z );
    
    // ручное приведение типов от d3_point_ptr к base_iface_ptr
    base_iface_ptr d3_point_to_base_iface( d3_point_ptr ptr );
    
    // ручное приведение типов от d3_point_ptr к sizible_iface_ptr
    sizible_iface_ptr d3_point_to_sizible_iface( d3_point_ptr ptr );
    


    Для 3D-точки я поменял местами обычные и виртуальные «методы», чтобы продемонстрировать, как работает виртуальность на разных структурах (не похожих по размещению своих элементов внутри).

    Так как у нас С компилятор, приходится вводить явное преобразование типов от point_t* и от d3_point_t* к base_iface* и sizible_iface* и наоборот. Это сделано для того, чтобы функции могли принимать указатель именно на интерфейс и не заботиться о том, что они работают с разными типами структур.

    Реализация функций для структур point_t и d3_point_t приведена ниже:
    point.с
    #include "std.h"
    
    static point_ptr base_iface_to_point( base_iface_ptr ptr );
    
    // реализация "виртуального метода" интерфеса base_iface
    static const char* point_get_name( base_iface_ptr ptr )
    {
        static const char* null_point_ptr = "Null point";
        static const char* some_point_ptr = "Some point";
    
        point_ptr casted_ptr = base_iface_to_point( ptr );
        
        const char* result = null_point_ptr;
        if( casted_ptr->x || casted_ptr->y )
        {
            result = some_point_ptr;
        }
    
        return result;
    }
    
    static point_ptr sizible_iface_to_point( sizible_iface_ptr ptr );
    
    // реализация "виртуального метода" интерфеса sizible_iface
    static unsigned int point_get_size( sizible_iface_ptr ptr)
    {
        point_ptr casted_ptr = sizible_iface_to_point( ptr );
    
        unsigned int size = (unsigned int)( sizeof( casted_ptr->x ) + sizeof( casted_ptr->y ) );
        return size;
    }
    
    // реализация обычного "метода" структуры point_t
    static void point_print_coordinates( point_ptr ptr )
    {
        printf( "x = %u, y = %u\n", ptr->x, ptr->y );
    }
    
    // конструктор point_t
    // инициализируем все указатели на функции внутренними (private) функциями
    point_t point_init( int x, int y )
    {
        point_t point;
    
        point.get_name = point_get_name;
        point.get_size = point_get_size;
        point.print_coordinates = point_print_coordinates;
    
        point.x = x;
        point.y = y;
    
        return point;
    }
    
    // это перечисление необходимо для приведения типов
    enum
    {
        num_get_name_offset = 0,
        num_get_size_offset = sizeof( get_name_func_ptr ),
    };
    
    // реализация приведения типов
    static point_ptr base_iface_to_point( base_iface_ptr ptr )
    {
        return (point_ptr)( (char*)ptr - num_get_name_offset );
    }
    
    base_iface_ptr point_to_base_iface( point_ptr ptr )
    {
        return (base_iface_ptr)( (char*)ptr + num_get_name_offset );
    }
    
    static point_ptr sizible_iface_to_point( sizible_iface_ptr ptr )
    {
        return (point_ptr)( (char*)ptr - num_get_size_offset );
    }
    
    sizible_iface_ptr point_to_sizible_iface( point_ptr ptr )
    {
        return (sizible_iface_ptr)( (char*)ptr + num_get_size_offset );
    }
    

    3d_point.c
    #include "std.h"
    
    static d3_point_ptr base_iface_to_d3_point( base_iface_ptr ptr );
    
    // реализация "виртуального метода" интерфеса base_iface
    static const char* d3_point_get_name( base_iface_ptr ptr )
    {
        static const char* null_point_ptr = "Null 3D point";
        static const char* some_point_ptr = "Some 3D point";
    
        d3_point_ptr casted_ptr = base_iface_to_d3_point( ptr );
    
        const char* result = null_point_ptr;
        if( casted_ptr->x || casted_ptr->y || casted_ptr->z )
        {
            result = some_point_ptr;
        }
    
        return result;
    }
    
    static d3_point_ptr sizible_iface_to_d3_point( sizible_iface_ptr ptr );
    
    // реализация "виртуального метода" интерфеса sizible_iface
    static unsigned int d3_point_get_size( sizible_iface_ptr ptr)
    {
        d3_point_ptr casted_ptr = sizible_iface_to_d3_point( ptr );
    
        unsigned int size = (unsigned int)
            ( sizeof( casted_ptr->x ) + sizeof( casted_ptr->y ) + sizeof( casted_ptr->z ) );
        return size;
    }
    
    // реализация обычного "метода" структуры d3_point_t
    static void d3_point_print_coordinates( d3_point_ptr ptr )
    {
        printf( "x = %u, y = %u, z = %u\n", ptr->x, ptr->y, ptr->z );
    }
    
    // конструктор d3_point_t
    // инициализируем все указатели на функции внутренними (private) функциями
    d3_point_t d3_point_init( int x, int y, int z )
    {
        d3_point_t point;
    
        point.get_name = d3_point_get_name;
        point.get_size = d3_point_get_size;
        point.print_coordinates = d3_point_print_coordinates;
    
        point.x = x;
        point.y = y;
        point.z = z;
    
        return point;
    }
    
    // это перечисление необходимо для приведения типов
    enum
    {
        num_get_name_offset = sizeof( d3_point_print_coordinates_func ),
        num_get_size_offset = num_get_name_offset + sizeof( get_name_func_ptr ),
    };
    
    // реализация приведения типов
    static d3_point_ptr base_iface_to_d3_point( base_iface_ptr ptr )
    {
        return (d3_point_ptr)( (char*)ptr - num_get_name_offset );
    }
    
    base_iface_ptr d3_point_to_base_iface( d3_point_ptr ptr )
    {
        return (base_iface_ptr)( (char*)ptr + num_get_name_offset );
    }
    
    static d3_point_ptr sizible_iface_to_d3_point( sizible_iface_ptr ptr )
    {
        return (d3_point_ptr)( (char*)ptr - num_get_size_offset );
    }
    
    sizible_iface_ptr d3_point_to_sizible_iface( d3_point_ptr ptr )
    {
        return (sizible_iface_ptr)( (char*)ptr + num_get_size_offset );
    }
    


    Итак, все внутренние функции имеют спецификатор static, что делает их невидимыми для внешнего пользователя.

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

    В приведенных примерах наполнение «виртуальных методов» несколько натянуто. Главное, что мы имеем внутри этих «методов» доступ ко всем элементам структуры.

    Пример использования

    А теперь покажу на примере, как можно воспользоваться такой виртуальностью.
    Сначала надо сформировать заголовочный файл, который будет использоваться в качестве precompiled header:
    std.h
    #pragma once
    
    // system
    #include "stdio.h"
    
    // program
    #include "base_iface.h"
    #include "sizible_iface.h"
    
    #include "point.h"
    #include "3d_point.h"
    

    std.с
    #include "std.h"
    


    И, собственно, сам пример:
    main.c
    #include "std.h"
    
    // функция, работающая с указателем на интерфейс base_iface
    void print_name( base_iface_ptr ptr )
    {
        printf( "name = \"%s\"\n", ptr->get_name( ptr ) );
    }
    
    // функция, работающая с указателем на интерфейс sizible_iface
    void print_size( sizible_iface_ptr ptr )
    {
        printf( "size = %u\n", ptr->get_size( ptr ) );
    }
    
    int main( int argc, const char* argv[] )
    {
        // конструируем несколько точек
        point_t null_point = point_init( 0, 0 );
        point_t some_point = point_init( 1, 5 );
    
        d3_point_t d3_null_point = d3_point_init( 0, 0, 0 );
        d3_point_t d3_some_point = d3_point_init( 0, 1, 0 );
    
        // выводим имя точки через интерфейс
        print_name( point_to_base_iface( &null_point ) );
        // выводим размер точки через интерфейс
        print_size( point_to_sizible_iface( &null_point ) );
        // печатаем координаты точки
        null_point.print_coordinates( &null_point );
    
        puts( "\n" );
    
        // повторяем это для всех оставшихся точек
        print_name( point_to_base_iface( &some_point ) );
        print_size( point_to_sizible_iface( &some_point ) );
        some_point.print_coordinates( &some_point );
    
        puts( "\n" );
    
        print_name( d3_point_to_base_iface( &d3_null_point ) );
        print_size( d3_point_to_sizible_iface( &d3_null_point ) );
        d3_null_point.print_coordinates( &d3_null_point );
    
        puts( "\n" );
    
        print_name( d3_point_to_base_iface( &d3_some_point ) );
        print_size( d3_point_to_sizible_iface( &d3_some_point ) );
        d3_some_point.print_coordinates( &d3_some_point );
    
        puts( "\nPress any key to exit...\n" );
        getchar();
        return 0;
    }
    


    Пояснений к примеру немного: мы объявили две функции, принимающие указатели на интерфейсы. Следовательно, прежде чем подать в эти функции указатели на наши структуры, мы приводим эти указатели к нужному типу.

    Выводы


    Скомпилировав исходники в VS2010 (работает С компилятор) и запустив полученную программу, мы увидим следующее:
    name = "Null point"
    size = 8
    x = 0, y = 0
    
    name = "Some point"
    size = 8
    x = 1, y = 5
    
    name = "Null 3D point"
    size = 12
    x = 0, y = 0, z = 0
    
    name = "Some 3D point"
    size = 12
    x = 0, y = 1, z = 0
    
    Press any key to exit...
    

    Работает! У нас есть «виртуальные интерфейсы», есть «множественное наследование» и есть proof-of-concept! Правда, код выглядит не так красиво, как хотелось бы. Это связано с тем, что мы проделали часть работы, которую в С++ за нас делает компилятор:
    • приведение типов
    • работа с таблицей виртуальных методов
    • передача внутрь «метода» указателя на эту же структуру (тот самый this)

    Но в целом прикладной код делает как раз то, что от него и хотелось. Где можно применить такую виртуальность? К примеру, в векторе разных по природе структур, но реализующих один и тот же интерфейс. Или несколько.
    Или в криптографии, когда можно использовать интерфейс хешей, в общем случае имеющих два метода: добавить еще данных и получить хеш от введенных данных.

    Ну и возможность просто создать структуру, а потом посмотреть на ней (через оператор "."), какие она предоставляет возможности («методы») с понятными именами функций и подсказкой, какие параметры нужно передавать, мне импонирует.

    Литература по теме


    1. Object-Oriented Programming With ANSI-C (xana4ok)
    2. Beautiful Code (Leading programmers explain how they think), Chapter 16, The Linux Kernel Driver Model: The Benefits of Working Together, by Greg Kroah-Hartman (burjui)
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 24

      +6
      Осталось таблицу виртуальных методов объявить как static, чтобы был только один экземпляр, да поставить на неё указатель в структуре и получится обычный vtable.
        0
        Можно, но я хотел избавиться от промежуточных сущностей.
          0
          Но увеличился расход памяти т.к. таблица хранится в каждом экземпляре класса.
            0
            Да, эта проблема есть. Однако в моем случае в конструкторе можно в зависимости от параметров назначать различные функции. С vtable так не получится.
            А во-вторых, расход памяти увеличится, если больше 1-го метода в интерфейсе. В простейших случаях можно и одним методом на интерфейс обойтись.
        +2
        К месту ли ваши модные мемы, а?
          0
          Возможно, не к месту. Но без них вообще не за что зацепиться в статье.
            +2
            В серьезной технической статье они немного глуповато выглядят, ИМХО.
              +3
              Они вообще глуповато выглядят, ИМХО.
                +5
                Убрал мемы. Не знал, что они послужат поводом сливать карму.
          +2
          Посмотрите, как написан Gtk.
            0
            Я скопировал их велосипед? :)
            Gtk большой, на что именно советуете посмотреть?
              0
              В какой-то мере. :)
              Точнее это GLib, см. GObject/GObjectClass, bit.ly/TmFYPh
                0
                Ну, ясное дело, что умные люди давно эту схему придумали и используют.
                Но, насколько я понял, все «объекты» в GTK+ наследуют один и тот же интерфейс: GObjectClass. Я же пытался реализовать множественное наследование. Возможно, и такое у разработчиков уже есть.
                  0
                  Нет, один объект может наследоваться от другого
                    0
                    А множественное наследование есть?
                      0
                      Вроде нет. Попробовал сейчас в Vala, получил ошибку — «Classes cannot have multiple base classes».
                        0
                        Зачит, от моего велосипеда хоть какая-то польза. :)
            +1
            О я так писал на первом курсе университета на паскале. Объекты мы еще не знали. Так вот я написал:
            const length=10;
            type
            vofunc = function (self);

            menu = record
            items: array[0..length];
            draw: vofunc;
            end;
            Где а потом добавлял метод и всегда первым параметром передавал саму структуру. у преподавателя глаза полезли на лоб.
              +7
              Я применил подобный подход для драйверов GPIO в библиотеке для дисплеев на контроллере HD44780 (сорцы, статья с объяснением). Думаю, более-менее опытному разработчику на C этот подход рано или поздно придёт в голову даже даже без знания C++ и ООП, так как естественен для C. А в ядре Linux он используется весьма активно: читаем книгу «Идеальный код» (Beatiful Code), главу 16 — «Модель драйверов ядра Linux: преимущества совместной работы» за авторством Грега Кроа-Хартмэна (Greg Kroah-Hartman).
                +2
                Добавил книгу в раздел литературы.
                +1
                Класс!
                  +4
                  Есть замечательная книга Object-Oriented Programming With ANSI-C bit.ly/T90E
                    +2
                    Добавил ссылку в раздел литературы.
                    +1
                    Спасибо за статью! Что-то начали на хабре снова появляться серьёзные технические вещи, что не может не радовать =)

                    Only users with full accounts can post comments. Log in, please.