Мне тут пришлось вспомнить, как же все таки надо писать на С, хотя работаю С++ программистом. И мне так не хватало классов и методов, что я стал думать, как можно приблизить С к С++. Зачем? Просто так, мозги размять.
Основное пожелание было следующим: хочется, чтобы в С работал примерно такой код:
По сути, A и В наследуют один и тот же (а может и несколько) интерфейс(ов). При этом можно на этих объектах вызывать виртуальные методы, действующие в зависимости от того, какой указатель был подан.
Кому интересно, что получилось в итоге (а в каком-то виде задача была решена) и кому интересно, как примерно в С++ реализуются виртуальные методы, прошу под кат.
Естественно, я не один такой умник. Методы на структурах реализуются через указатели на функции. Вот одно из решений, от которого мы будем отталкиваться.
Какие есть недостатки у такого решения?
Вместо отдельной структуры, являющейся таблицей методов, положим в нашу структуру явно указатели на функции. Причем положим в самое начало для простоты.
Если нужно сделать множественное «наследование», будем класть последовательно сначала методы одного интерфейса, затем методы второго и т.д.
В функции, принимающие указатели на интерфейс, будем передавать указатель на первый метод интерфейса в структуре. Так как этот метод будет иметь специфичную для структуры реализацию, мы можем заложиться на смещение этого адреса от начала адреса структуры в целом.
Недостаток здесь очевиден: надо учитывать смещения в структуре. Следовательно, нужно либо упаковывать структуру, либо каким-то образом узнавать смещение. Я сделал проще: забил и считал, что выравнивания по размеру указателя будет достаточно.
Далее будет приведена реализация задуманного при помощи precompiled headers и VS2010 в режиме С компилятора.
Итак, введем два интерфейса.
Первый, base_iface, позволяет вызывать метод, возвращающий строку с описанием объекта.
Второй, sizible_iface, позволяет узнать размер объекта.
Примеры надуманные, но нам сойдет.
Сделаем две структуры, point_t и d3_point_t, представляющие собой точку в двумерном и трехмерном пространстве соответственно. Каждая структура содержит координаты точки, а также три указателя на функции: «получить имя объекта», «получить размер объекта» и «напечатать координаты точки».
Первый указатель — виртуальный метод, «наследование» интерфейса base_iface.
Второй указатель — виртуальный метод, «наследование» интерфейса sizible_iface.
Третий указатель — обычный метод, не виртуальный.
Заголовочные файлы приведены ниже:
Для 3D-точки я поменял местами обычные и виртуальные «методы», чтобы продемонстрировать, как работает виртуальность на разных структурах (не похожих по размещению своих элементов внутри).
Так как у нас С компилятор, приходится вводить явное преобразование типов от point_t* и от d3_point_t* к base_iface* и sizible_iface* и наоборот. Это сделано для того, чтобы функции могли принимать указатель именно на интерфейс и не заботиться о том, что они работают с разными типами структур.
Реализация функций для структур point_t и d3_point_t приведена ниже:
Итак, все внутренние функции имеют спецификатор static, что делает их невидимыми для внешнего пользователя.
Реализация виртуальных методов тривиальна: получая указатель на интерфейс, мы выполняем работу С++ компилятора:
В приведенных примерах наполнение «виртуальных методов» несколько натянуто. Главное, что мы имеем внутри этих «методов» доступ ко всем элементам структуры.
А теперь покажу на примере, как можно воспользоваться такой виртуальностью.
Сначала надо сформировать заголовочный файл, который будет использоваться в качестве precompiled header:
И, собственно, сам пример:
Пояснений к примеру немного: мы объявили две функции, принимающие указатели на интерфейсы. Следовательно, прежде чем подать в эти функции указатели на наши структуры, мы приводим эти указатели к нужному типу.
Скомпилировав исходники в VS2010 (работает С компилятор) и запустив полученную программу, мы увидим следующее:
Работает! У нас есть «виртуальные интерфейсы», есть «множественное наследование» и есть proof-of-concept! Правда, код выглядит не так красиво, как хотелось бы. Это связано с тем, что мы проделали часть работы, которую в С++ за нас делает компилятор:
Но в целом прикладной код делает как раз то, что от него и хотелось. Где можно применить такую виртуальность? К примеру, в векторе разных по природе структур, но реализующих один и тот же интерфейс. Или несколько.
Или в криптографии, когда можно использовать интерфейс хешей, в общем случае имеющих два метода: добавить еще данных и получить хеш от введенных данных.
Ну и возможность просто создать структуру, а потом посмотреть на ней (через оператор "."), какие она предоставляет возможности («методы») с понятными именами функций и подсказкой, какие параметры нужно передавать, мне импонирует.
Основное пожелание было следующим: хочется, чтобы в С работал примерно такой код:
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)
Но в целом прикладной код делает как раз то, что от него и хотелось. Где можно применить такую виртуальность? К примеру, в векторе разных по природе структур, но реализующих один и тот же интерфейс. Или несколько.
Или в криптографии, когда можно использовать интерфейс хешей, в общем случае имеющих два метода: добавить еще данных и получить хеш от введенных данных.
Ну и возможность просто создать структуру, а потом посмотреть на ней (через оператор "."), какие она предоставляет возможности («методы») с понятными именами функций и подсказкой, какие параметры нужно передавать, мне импонирует.
Литература по теме
- Object-Oriented Programming With ANSI-C (xana4ok)
- 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)