Implications of inheritance: OOP is Bad

Although the primary premise of Object-Oriented Programming may be simple, it forces certain design decisions that you otherwise wouldn’t make. Structuring your program with an OOP mindset already restricts your thinking, but these extra implications make thinking outside the object box much harder.

Modern programming languages that “support” OOP force you to use their restrictions throughout your program.

What is OOP?

In the everyday sense, OOP is a software strategy where functions and data are tied together. I don’t have an issue with that. If that’s all that OOP did, then it would merely be an alternative syntax for accomplishing this:

// not OOP (?)
struct my_struct {
    int a, b, c;
}

int calculate_sum(my_struct& self) {
    return self.a + self.b + self.c;
}

//~ usage ///////////////////

my_struct object = {1, 2, 3};
int res = calculate_sum(object);
// OOP 
class my_class {
    int a, b, c;
}

int my_class::calculate_sum(/* implicit self */) {
    return self.a + self.b + self.c;
}

//~ usage ///////////////////

my_class object = {1, 2, 3};
int res = object.calculate_sum();

So this isn’t the whole story. A program being “OOP” implies other things, mainly inheritance.

What is inheritance?

Inheritance is the ability of a class to extend other classes. It allows you to add methods and members on top of a generic base class.

It looks something like this.

// OOP
class my_base_class {
    int a, b;
}
class my_class : my_base_class {
    // int a, b; are automatically imported from the base class
    int c;
}

But you can still do this without OOP.

// not OOP (?)
struct my_struct {
    int a, b;
}
struct my_struct {
    my_base_struct base;
    int c;
}

The difference is that you explicitly type object.base.a + object.base.b, but a language feature could make it identical.

The “non-OOP” way is so far superior:

  • Control over memory layout
  • Explicit without being cumbersome
  • Allows composability (although some languages have “multiple-inheritance”)

So what’s the catch

OOP inheritance allows you to override functions.

// OOP
class my_base_class {
    virtual string to_string() {
        return "base_class";
    }
}
class my_class : my_base_class {
    string to_string() { // overridden function
        return "my_class";
    }
}

The “magic” part about OOP is that casting an object to its parent class retains the overridden methods in the parent class.

void print_string(my_base_class obj) {
    print(obj.to_string());
}

my_class object = new my_class();
// object will implicitly cast to 'my_base_class'
print_string(object); // prints "my_class"

Did you notice something?
For this functionality to work, we created the object with a constructor called via new my_class();.

So RAII, Resource acquisition is initialization, that’s the catch that makes OOP unique.

Other ways that don’t use RAII

1. Function pointers

Function pointers manually do what the RAII and virtual methods do “automatically” for us.

typedef string to_string_function_pointer_t(void* self);

//////////////////////////////////////////////////
// Replace virtual function with function pointer defined above

struct my_base_struct {
    to_string_function_pointer_t* to_string;
}
struct my_struct {
    my_base_struct base;
}

//////////////////////////////////////////////////
// Make the to_string functions

string to_string_base_struct(void* self_void) {
    my_base_struct* self = self_void;
    return "base_class";
}

string to_string_my_struct(void* self_void) {
    my_struct* self = self_void;
    return "my_class";
}

//////////////////////////////////////////////////
// Make "constructors"

my_base_struct* make_my_base_struct() {
    my_base_struct* res = malloc(sizeof(*res));
    *res = (my_base_struct){ .to_string =  to_string_base_struct};
    return res;
}

my_struct* make_my_struct() {
    my_struct* res = malloc(sizeof(*res));
    *res = (my_base_struct){ .to_string =  to_string_my_struct};
    return res;
}

Usage then remains the same.

void print_string(my_base_struct base) {
    print(base.to_string());
}

my_struct object = make_my_struct();
print_string(object.base); // prints "my_class"

This example shows how much work the OOP language is doing for us. C makes it apparent how intricate of a solution this is.

2. Enums

enum struct_type {
    TYPE_MY_BASE_STRUCT,
    TYPE_MY_STRUCT,
};

struct my_base_struct {
    struct_type type;
}

string to_string(my_base_struct* obj) {
    switch (obj.type) {
    case TYPE_MY_BASE_STRUCT: return "my_base_struct";
    case TYPE_MY_STRUCT: return "my_struct";
    default: return "invalid type";
    }
}

void print_string(my_base_struct base) {
    print(to_string(base));
}

my_base_struct object = {.type = TYPE_MY_STRUCT};
print_string(object.base); // prints "my_class"

Why I think the non-RAII ways are better

1. Function pointers

As you saw, the function pointer strategy was pretty painful. However, it’s very explicit. In the cases where we genuinely need a fully obfuscated API, this is perfect.

Functions are a fundamental concept and are easy to reuse. The to_string_my_struct() can be used by any other function, which many OOP languages will not let you do.

string to_string_my_struct(my_struct* obj) {
    return to_string_my_base_struct(obj.base) + int_to_string(obj.c);
}


Many child structs can compose multiple different function pointers. Typical OOP syntax does not allow this.

I personally only find function pointers useful with dynamically loaded code. The OOP mindset fails to make this obvious.

Through ease of composability and lack of required language features—I would argue that for dynamic cases, even without having gotten to the thick part of why RAII is bad, function pointers are superior.

2. Enums

The enum example has roughly the same amount of code as the OOP example, maybe slightly more. The enum solution states its intent clearly, as it doesn’t use advanced language features.

You might have noticed I never created the my_struct in the enum example. This happened because we don’t need multiple types anymore.

The enums let us compose all our behavior in a single type!

struct entity {
    entity_type type;
    struct  {
        int hp;
        hit_box hitbox;
        vec2 velocity;
        vec2 position;
    } common;

    struct  {
        entity_ref current_target;
    } common_enemy;
}

If you have never done this before, it is incredibly useful. All functions collapse into one, and the different data members toggle the related branches.

The code becomes a lot more uniform. All the struct members are directly visible, and all the branches and composed behavior get placed next to each other. In extreme cases, multiple files with tens of classes and functions become a single large function. Utilizing a centralized vs decentralized approach might be a personal preference, but performance and code sharing are objectively better with the latter approach.

Additionally, if you’re concerned about the memory usage of having one large struct, you can use a “tagged union”.

3. You can combine the two above

struct entity {
    entity_type type;
    struct  {
        int hp;
        hit_box hitbox;
        vec2 velocity;
        vec2 position;
    } common;

    struct  {
        entity_ref current_target;
    } common_enemy;

    void* extra_entity_data;
    entity_function_poitner_t* update;
    entity_function_poitner_t* on_spawn;
    entity_function_poitner_t* on_death;
}

You can incorporate the function pointer approach without removing anything in an enum-based type. Doing this lets static code remain straightforward while providing dynamic functionality.

Finally: Problems with RAII

OOP and RAII bot suffer from a problematic thinking pattern where every object needs to be constructed, destroyed, and handled individually. In reality, this is almost never the case. When I talk to people tainted by the stains of OOP, they often end up with scattered types and functions and useless boilerplate.

Learning to chunk elements with the same lifetime together makes memory management a breeze.

Want more videos like the one above? My list of software rants worth watching.

Due to OOP languages supporting this single feature—namely poor automation of function pointers—they force your entire program to be written in a sub-optimal way. Languages like C++ allow a hybrid approach due to its history as an extension of C. Most OOP languages are not like this.

When I use a language with native OOP features, I find myself searching “How to do struct literal in X”. Only to realize I have to write a constructor, despite not even using virtual functions.

What about encapsulation

You might have noticed that I didn’t mention encapsulation. That’s because encapsulation is stupid, and abusing it is harmful.

If you need encapsulation, you should use an opaque type.

Ensuring individual members are only used internally can be done by prefixing the name with _ or _INTERNAL_.

· writing, programming, C