OOP in C
staff.washington.edu
staff.washington.edu
https://www.cs.rit.edu/~ats/books/ooc.pdf
(edit: link updated to point to author's website)
I only read a few chapters then got sidetracked, but man I learnt so much stuff from reading just a bit
i already knew all the C concepts he was using but it never occurred to me to use them in that way.
The book is very well-written and easy to understand, and also very approachable even for someone who is not so experienced with C, like myself
I want to start over someday
Never truly finished it, but it works well :)
Python because of the insistence of declaring that explicit self argument, the pointer to the struct for the object.
JavaScript because of the prototype based OO. You can change the meaning of any field in an OO C struct if you know how to handle it later.
For example, the first example in the PDF implements a Set. Everything is "void". This loses typing and will make code hard to read and maintain. The way inheritance is mimicked is hideous and inefficient. The PDF also heavily uses function pointers, which can be slow (because they aren't easily inlined) and waste memory (because each pointer costs 8 bytes on x64).
In all, most large C projects are doing OOP at some level. However, C is not C++ and is not intended for providing all OOP features like inheritance. This book should probably be titled "Wrong ways to OOP in C".
PS: The book does have some interesting ideas, but those are the "clever" things you will have to unlearn later. The author provides the source code for the book [1]. Have a look at the string implementation in Chapter Two. That is the most arcane and inefficient string implementation I have seen.
Don’t listen to prescriptions from strangers on HN, how about? Myself included. :-)
> However, C is not C++ and is not intended for providing all OOP features like inheritance.
There are parts of the ISO C standard specifically included to allow for inheritance. Consider:
typedef struct{ int x; }struct_a;
typedef struct{ struct_a inherited; int y; }struct_b;
void function(struct_a *s) { s->x = 0; }
Here struct_b inharates struct_a. You can call the function with either a pointer to struct_a or struct_b (either by casing or void pointer), because the standard specifically states that there can be no padding before the first member of a structure, so that the first member of a structure should have the same pointer as the structure itself. This part of the spec was written with the use case of inheritance in mind. (there is some people who don't read the spec that way, but this is the intention)
Sorry for being nitpicky... but that's pretty much what we do in the wg14....
void foo(struct_a *a, struct_b *b) { a->x = 0; b->x += 1; a->x = 0; }
Strict aliasing allows the compiler to elide the second a->x = 0. But if we use your inheritance scheme using casting of pointers, we could have a == b.i was basically reiterating my comment from there.
> using a function pointer has unnecessary overhead
This is true, it's inefficient. But for a lot of application, also irrelevant at performance level, and would provide a good abstraction.
Traits are like interfaces in OOP languages (simplified), the equivalent of a class would be a struct + all the impls of that struct. And that thing cannot be extends, a struct is fixed when defined and nobody can extend it, you can use composition that is the correct to me way to go.
Traits describe the methods implemented, and you can override with specific implementations or inherit the default, from a level up.
It got a bad reputation because it was abused. Bad practice is using inheritance when a tuple or a map would suffice.
Inheritance (from implementation, I've nothing against implementation of interfaces or inheritance from abstract base classes) has a ton of problems, more importantly the fact that it makes the code more difficult to understand and to evolve.
Composition on the other hand is something more natural, even if we think about real life: you don't usually take an object and "extend" it, you take multiple object and use them together to build something!
I don't know why we need to be so judgmental about it.
I think inheritance is especially good if you have an interface (in the OO sense, like some languages use an "interface" keyword for), but you have some common or default methods, which a specific implementation may or may not override, or maybe there is some boiler plate or tedium where the most common implementation might belong in a base class. I think this is handy for something like a device driver.
> has a ton of problems
this may be a good reference with examples (and some good dose of nuance) on the subject:https://blog.gougousis.net/oop-inheritance-what-when-and-why...
Right... There is an animal in a dog.
You can do this structure in C, but more common is the struct of function pointers which avoids a level of indirection at the cost of object size.
list->init(); → list->init(list);
list.init(); → list.init(&list);
[1] https://sentido-labs.com/en/library/cedro/202106171400/#back...I normally avoid the function pointer overhead, which can be done with _Generic:
#define append(VEC, START, END) _Generic((VEC), \
Vec_float*: append_Vec_float, \
Vec_str*: append_Vec_str, \
Vec_cstr*: append_Vec_cstr \
)(VEC, START, END)
https://sentido-labs.com/en/library/cedro/202106171400/#loop...But list->init(list) might be a simpler solution for most cases, and compatible with C89/C99.
git clone -b self https://github.com/Sentido-Labs/cedro.git
cd cedro
make bin/cedro # Just “make” will build cedrocc etc.
bin/cedro - <<' EOF' # Mind the indentation.
#pragma Cedro 1.0 self
list->init();
list.init();
list->append(123);
list.append(123);
EOF
The “self” flag after “#pragma Cedro 1.0” activates the “self” macro, because it should not be done by default.Result:
list->init(list);
list.init(&list);
list->append(list, 123);
list.append(&list, 123);
I’ll try it out for a few days and if it works well in practice I’ll document it and merge it into master.You followed the path of C++.
To some extent yes, I know about cfront.
This is just another iteration on that old idea.
Which is fine! Should be interesting.
And how do you for example, create an array of objects, like one can easily do in c++?
https://en.wikipedia.org/wiki/COLA_(software_architecture)
Huh, that page was deleted in Dec 22. "concern was: Old research project. Unable to locate any details. VPRI institute is dead. Been on the cat:nn list since March 2009. No new updates."
Goddamned deletionist activist WP editors tearing down the human knowledge base.
(If you don't know VPRI is (was?) Alan Kay's research org. I think it's a bit notable and important.)
ANYWAY
From the search result (in DDG) snippet I can get the first two sentences of the deleted page:
> "COLA" stands for "Combined Object Lambda Architecture". [1] A COLA is a self-describing language in two parts, an object system which is implemented in terms of objects, and a functional language to describe the computation to perform. [2]
It's a very simple system that gives you the basis for both OOP and Lisp-like semantics. It's fun!
You can see it here: https://piumarta.com/software/cola/
Or check out the VPRI reports, etc.:
https://web.archive.org/web/20220819075633/https://www.vpri....
Ironically their website appears to be down at the moment.
https://en.wikipedia.org/wiki/Resource_acquisition_is_initia...
(GObject is of course also object-oriented, but IMO hardly counts as C programming in how it works, it’s more of a separate OO language hand-translated into C—Vala is basically that language, finally implemented years later.)
It had to be done a certain way, because stack conventions were all over the place, back then.
https://www.ncbi.nlm.nih.gov/IEB/ToolBox/SDKDOCS/VIBRANT.HTM...
The code is pretty well written and probably can be used in a teaching environment, like the original article describes. But I am very bad in teaching…
Surely in C one does use tables of function pointers to implement some aspects of OOP. However, a good library limits it and rather exposes some structs instead of requiring multiple virtual methods calls to archive a similar effect.
Preferably using int/float which should be atomic on X86 and hopefully on modern ARM too, don't know about RISC-V yet.
I have not looked through the source myself but thought this might be an interesting reference.
This is relatively common in software with a modular architecture that is intended to run on embedded systems and whose design dates back from a time when C++ standard library was not really well supported on such systems. This is also useful when performance is highly important and you can't afford the C++ vtable overhead.
It works, but all the machinery has to be handled manually. It's very, very easy to make a mistake. The question is, why do this? Just use C++ as "C with classes" and you'll be much better off.
and the question: "so much manual/repetitive work, why do this"... is still valid.
The main advantage of C++ isn't really OOP, its all the standard library features and smart pointers, but those are not always applicable (like when you want to manage memory as efficient as possible). So using C is often preferred.
Otherwise, ADT can also refer to “algebraic data type” which means the ability to compose types together thereby adding or multiplying the different values the resulting type can take. Product types (aka structs) multiply over their fields; two int fields when taken together can take on (# different values of int) * (# different values of int) different values. Sum types add over their variants: a Rust enum “enum OptionalInt { Some(i32), Maybe(i32), None }” can take (# different values of i32) + (# different values of i32) + 1 different values.
Most languages have structs and that’s the “product type” sorted, so ADT generally refers to a language having tagged enums. C doesn’t have that but you can emulate it (quite badly) with unions and enums. Good examples of languages with ADTs are OCaml, Haskell, Rust.
Abstract data type is a type where you don't get direct access to the information contained in it. The encapsulation is what makes it "abstract".
There's no point in changing anything.
The crucial difference between an abstract data type and a concrete data type is that the content is hidden in the former case.
BUT: inheritance. C++ has subclasses (`class Manager : Employee`) and virtuals / vtable lookup on pointer access (`employee->name()` which calls `Manager::name` or `Employee::name` based on type).
What is the typical equivalent in C? Hand-rolled vtables? Or is there a general paradigm that helps to avoid the need for it in C?
I ask as a seasoned C++ and Java developer looking to improve some C code which uses this pattern in the extreme and needs some refactoring. My intuition is composition rather than inheritance, although isA is more intuitive than hasA for this code-base.