lp0 on fire

My personal blog

C89-OOP: Conclusion

Now that we have a good idea how each individual system works, lets translate a simple C# class to C89 using "best pratices" such as the principle of program to an interface, not the implementation.

Example class

This is the simple program we'll be implementing.

public interface IFoo
{
    public int X { get };
    public void Print();
}

public class Foo : IFoo
{
    public int X { get };

    Foo(int x)
    {
        X = x;
    }

    public void Print()
    {
        Console.WriteLine(X);
    }
}

public class Program
{
    public static void Main()
    {
        Foo foo = new Foo(1);
        foo.print();
    }
}

Foo inherits from object by default in C#, so we'll be including the type system. The public int X { get }; is property, so the getter is generated and thus we need to include the getX function as well.

Implementation

This will be the type system:

typedef struct Type Type;

struct Type
{
    int (*getId)(void);
};

void Type_setId(int* typeId)
{
    static int id = 0;

    if (*typeId == 0)
    {
        *typeId = ++id;
    }
}

As for the IFoo interface:

typedef struct IFoo IFoo;

struct IFoo
{
    int (*getX)(IFoo* self);
    void (*print)(IFoo* self);
};

Now that that's all in place, the Foo class itself:

typedef struct Foo Foo;

struct Foo
{
    Type* type;
    IFoo* ifoo;
    int x;
};

int Foo_getTypeId(void)
{
    static int typeId = 0;

    Type_setId(&typeId);
    return typeId;
}

int Foo_getX(IFoo* self)
{
    Foo* foo = (Foo*)self;
    return foo->x;
}

void Foo_print(IFoo* self)
{
    Foo* foo = (Foo*)self;
    printf("%d\n", foo->x);
}

void Foo_ctor(Foo* self, int x)
{
    static Type typeinfo = {
        Foo_getTypeId
    };
    static IFoo ifoo = {
        Foo_getX,
        Foo_print
    };

    self->type = &typeinfo;
    self->ifoo = &ifoo;
    self->x = x;
}

Foo* Foo_new(int x)
{
    Foo* self = malloc(sizeof(Foo));
    Foo_ctor(self, x);
    return self;
}

void Foo_delete(Foo* self)
{
    free(self);
}

At last, the Program.Main:

int main(void)
{
    Foo* foo = Foo_new(1);
    foo->ifoo->print((IFoo*)foo);
    Foo_delete(foo);
    return EXIT_SUCCESS;
}

That's all the code generated in a best-case scenario (because this isn't running inside a VM / runtime that uses GC, exceptions, reflection, RAII and all those other fancy systems nor does it include a fat object type with many base functions).

Alternative

So why don't we just write this:

struct Foo
{
    int x;
};

int main(void)
{
    struct Foo foo = { 1 };
    printf("%d\n", foo.x);
    return EXIT_SUCCESS;
}

Much simpler, faster and easier to understand.

Conclusion

See how convoluted the OOP solution is internally? It is rarely something other people think about when using OOP because it's all hidden, and the question of complexity and performance is not often raised. Not many seem to care.

Many programs don't need the power nor the flexability of OOP, especially not the hidden cost that comes with it. Many OOP programs are big and complex in nature because OOP is. In OOP you need abstractions, principles and code standards because otherwise OOP is not managable.

This is not the case for C, Go and other imperative languages. You don't need those things the language and code itself is simple enough to be understood without them. If your application ever gets sufficient large enough that you need abstraction, then it's much easier to manage in an imperative language because the fundumentals are simpler.

Let's not forget that OOP kills caching because it embeds multiple data structures through inheritance that could be split instead. However, this line of thinking is not encouraged by the language's design itself because you're supposed to think in contained "objects", not in processing raw data.