📜 ⬆️ ⬇️

Add some virtuality to C

I then had to remember how to still need to write in C, although I work as a C + + programmer. And I lacked classes and methods so much that I began to think how to bring C to C ++ closer. What for? Just so the brains stretch.
The main wish was as follows: I would like to work in C like this:

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" } 


In fact, A and B inherit the same (and maybe several) interface (s). At the same time, it is possible to invoke virtual methods on these objects, depending on the pointer that was supplied.
')
Who cares what happened in the end (and in some form the problem was solved) and who is interested in how virtual methods are implemented approximately in C ++, I ask for cat.




What is ready


Naturally, I'm not the only one smart guy. Methods on structures are implemented through function pointers. Here is one of the solutions from which we will build.

What are the disadvantages of such a solution?



How will we solve the problem?


Instead of a separate structure that is a table of methods, we put in our structure explicitly pointers to functions. And put it at the very beginning for simplicity.
If you need to do multiple "inheritance", we will first put methods of one interface, then methods of the second, etc.

In the function accepting pointers to the interface, we will pass a pointer to the first interface method in the structure. Since this method will have an implementation specific to the structure, we can build on the offset of this address from the beginning of the address of the structure as a whole.

The disadvantage here is obvious: we must take into account the bias in the structure. Therefore, you need to either pack the structure, or somehow recognize the offset. I made it easier: I scored and thought that aligning to the size of the pointer would be enough.

Below is the implementation conceived using precompiled headers and VS2010 in compiler mode C.

Go!


Interfaces

So, we will enter two interfaces.
The first, base_iface , allows you to call a method that returns a string describing the object.
The second, sizible_iface , lets you know the size of the object.
Examples are far-fetched, but we will get away.
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; 


Classes

We make two structures, point_t and d3_point_t , representing a point in two-dimensional and three-dimensional space, respectively. Each structure contains the coordinates of a point, as well as three function pointers: “get object name”, “get object size”, and “print point coordinates”.

The first pointer is a virtual method, "inheriting" the base_iface interface.
The second pointer is a virtual method, “inheritance” of the sizible_iface interface.
The third pointer is the usual method, not the virtual one.

Header files are listed below:
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 ); 


For the 3D point, I swapped the usual and virtual “methods” to demonstrate how virtuality works on different structures (not similar in placing their elements inside).

Since we have a C compiler, we have to introduce an explicit type conversion from point_t * and from d3_point_t * to base_iface * and sizible_iface * and vice versa. This is done so that the functions can take a pointer to the interface and not worry about the fact that they work with different types of structures.

The implementation of the functions for the point_t and d3_point_t structures is given below:
point.c
 #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 ); } 


So, all internal functions have the static specifier, which makes them invisible to the external user.

The implementation of virtual methods is trivial: obtaining a pointer to an interface, we perform the work of the C ++ compiler:

In the examples given, the content of the “virtual methods” is somewhat strained. The main thing is that within these "methods" we have access to all elements of the structure.

Usage example

And now I will show you with an example of how you can take advantage of such virtuality.
First you need to create a header file that will be used as a 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.s
 #include "std.h" 


And, actually, the example itself:
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; } 


There are a few explanations for example: we declared two functions that take pointers to interfaces. Therefore, before submitting pointers to our structures to these functions, we bring these pointers to the required type.

findings


Compiling the source code in VS2010 (works with the compiler) and running the resulting program, we will see the following:
 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... 

Works! We have “virtual interfaces”, there is “multiple inheritance” and there is a proof-of-concept! True, the code does not look as beautiful as we would like. This is due to the fact that we have done some of the work that the compiler does for us in C ++:

But in general, the application code does just what it wanted from him. Where can I apply this virtuality? For example, in a vector of structures of different nature, but implementing the same interface. Or a few.
Or in cryptography, when you can use a hash interface, generally having two methods: add more data and get a hash from the entered data.

Well, the opportunity to simply create a structure, and then look at it (through the "." Operator), what capabilities it provides ("methods") with clear names of functions and a hint, what parameters need to be passed, I am impressed.

Related Literature


  1. Object-Oriented Programming With ANSI-C ( xana4ok )
  2. Beautiful Code (Greg Kroah-Hartman ( burjui ))

Source: https://habr.com/ru/post/155439/


All Articles