Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Object Orientation in C

178 views
Skip to first unread message

David Kleinecke

unread,
Jun 12, 2017, 1:51:18 PM6/12/17
to
C is not an object oriented language (see C++). But object-
orientation is an organizing philosophy and a set of rules.
In OO organization the focus is on the data - rather than
the code. In classic C it was not quite clear where the
persistent data lived. One reading would be that it lived
in the set of top-level declarations in a translation unit
(and in static declarations in functions). But that reading
remains unsatisfactory because it cannot handle more than
one set of data at a time.

Suppose I wish to simultaneously run two instances of the
same program with different data. Unless I take special pains
I must load the code for the program twice (for well-known
reasons). The special pains I must take can be object
orientation.

The C compatible solution to the problem of more than one set
of data sharing the same code is:
Collect all the data into a struct,
Create instances of that struct on the heap,
Set the first variable of each of the methods (functions
making up the code) to a pointer to an instance.
It is easy enough to add details here like having struct
members be pointers to further structs.

This solution works well in isolation when all the instances
are exactly the same methodwise but such a simple situation
seems to be rather rare (although "tabbed" programs seem to
fit). I am about to describe my notion of how more complex
situations can be handled. This is a work in progress and I
welcome comments and reserve my right to change my mind about
this or that.

1. Each translation unit describes a single type (called
"class" in recent OO literature) and nothing about the type is
described elsewhere

2. The name of the type is used as the name of the
translation units: "Name.c" and "Name.h".

3. Type names are in camel code with initial caps. For example:
GeditNotebookPopupMenu

4. All the non-extern non-static functions in a translation
unit have names (equals are namespaced) of the form: the unit's
type name followed by lower case method names with words
separated by underlines. For example:
GeditNotebookPopupMenu_init (GeditNotebookPopupMenu* menu)

5. The first argument of such a function is always a pointer to
an instance of unit type.

6. Names of other things like typedefs and enums are of the same
form (point 4) as functions.

I have possibly missed some requirements - I will add them when I
become aware of them.

Single parent Inheritence is obvious. The inheriting struct
starts with an instance of the inherited struct. More inheriting
can also be done with more fussing.

In my opinion this is all that is needed to capture the basic
idea of object orientation. Things like polymorphism are optional
luxuries we can live without.

PS: Making the first arguments of type void* instead of pointers
to the proper types has some potential and might be better than
what I have suggested.

Pascal J. Bourguignon

unread,
Jun 12, 2017, 3:45:28 PM6/12/17
to
David Kleinecke <dklei...@gmail.com> writes:

> C is not an object oriented language (see C++). But object-
> orientation is an organizing philosophy and a set of rules.
> In OO organization the focus is on the data - rather than
> the code. In classic C it was not quite clear where the
> persistent data lived. One reading would be that it lived
> in the set of top-level declarations in a translation unit
> (and in static declarations in functions). But that reading
> remains unsatisfactory because it cannot handle more than
> one set of data at a time.
>
> Suppose I wish to simultaneously run two instances of the
> same program with different data. Unless I take special pains
> I must load the code for the program twice (for well-known
> reasons). The special pains I must take can be object
> orientation.
>
> The C compatible solution to the problem of more than one set
> of data sharing the same code is:
> Collect all the data into a struct,
> Create instances of that struct on the heap,
> Set the first variable of each of the methods (functions
> making up the code) to a pointer to an instance.
> It is easy enough to add details here like having struct
> members be pointers to further structs.

There are countless systems to add Object Orientation to C.
Just use one of them!

The most famous ones are Objective-C and C++, but we can agree that at
least C++ forked into an entirely different programming language, so
let's discount it.

Nonetheless, you could consider Objective-C (not only on Apple systems,
it's included in gcc and clang), or some other systems, more or less
hacky, you can find on github and elsewhere.
Notice that the problems you have when you want to load two different
programs in the same process, or multiple instances of the same program
in the same process, are not really solved by OOP, when you still allow
global variables and singleton objects. (And all programs have singleton
objects, if only the object that represent the program, which is often
confused with the object that represents the interface with the system
in object oriented frameworks (the so called "Application" class).


Here you have another solution: just keep each of your object in a
separate process, and implement the message sending between object as an
IPC. You get CORBA (but also dbus, rmpi and numerous other solutions).
ie. there are already solutions.

--
__Pascal J. Bourguignon
http://www.informatimago.com

David Kleinecke

unread,
Jun 13, 2017, 1:49:23 PM6/13/17
to
I was describing a way to use OOP organizational techniques
in a simple C implementation.

If there really are situations that demand complex OOP structures
C is not the correct language to use. I remain unconvinced that
such complex situations actually exist.

But specialized applications - like games - can justify specialized
languages. C is a general purpose tool.

GOTHIER Nathan

unread,
Jun 13, 2017, 7:45:01 PM6/13/17
to
On Mon, 12 Jun 2017 21:45:18 +0200
"Pascal J. Bourguignon" <p...@informatimago.com> wrote:

> There are countless systems to add Object Orientation to C.
> Just use one of them!

I don't think so. Apart from the GObject model there is no Object
Orientation in C.


> The most famous ones are Objective-C and C++, but we can agree that at
> least C++ forked into an entirely different programming language, so
> let's discount it.

That's clearly not C but conterfeited languages with C marketing.

Pascal J. Bourguignon

unread,
Jun 13, 2017, 8:06:57 PM6/13/17
to
It's only a matter of a few macros.

Chad

unread,
Jun 14, 2017, 9:51:08 PM6/14/17
to
There is a Computer Scientist who tried to argue that OOP is just message passing, local data retention, and late binding. He also argues that the term "Object" should have never been invented. Unironically, the only person on this planet that seems to have taken him seriously was the late Steve Jobs. This is evident because these same ideas appear in the various software developer kits that have been produced by the late Mr. Jobs.

Pascal J. Bourguignon

unread,
Jun 14, 2017, 11:49:22 PM6/14/17
to
1- this Computer Scientist is the inventor of OOP himself! Alan Kay.
http://wiki.c2.com/?AlanKayOnMessaging

2- Steve Jobs never programmed anything, much less any Objective-C
stuff. The impression you have about Objective-C comes from the fact
that the object model implemented by Objective-C is very close to
that of Smalltalk, Alan Kay's brainchild.

However, you will notice that in all common OOP languages, message
passing is implemented as synchronous function calls (methods always
return results). If messaging really was taken seriously, then there
would be no result and message sending would always be asynchronous.

(There are languages and systems where it's the case, but they're not
common).

Noob

unread,
Jun 15, 2017, 9:11:16 AM6/15/17
to
On 12/06/2017 19:51, David Kleinecke wrote: [snip]

I've had the following PDF open on my desktop for weeks.

Object-Oriented Programming With ANSI-C
https://www.cs.rit.edu/~ats/books/ooc.pdf

David Brown

unread,
Jun 15, 2017, 10:16:11 AM6/15/17
to
Code that uses "void *" pointers to emulate objects is just wrong. Put
them in structs so that the compiler has half a chance of checking your
code.

supe...@casperkitty.com

unread,
Jun 15, 2017, 11:00:19 AM6/15/17
to
On Thursday, June 15, 2017 at 9:16:11 AM UTC-5, David Brown wrote:
> Code that uses "void *" pointers to emulate objects is just wrong. Put
> them in structs so that the compiler has half a chance of checking your
> code.

On implementations that will extend the CIS guarantees to include structures
like:

struct BASICFOO {
struct BASICFOO_TYPE_INFO *typ;
...

struct ADVANCEDFOO {
struct ADVANCEDFOO_TYPE_INFO *typ;
...

where the TYPE_INFO objects in turn share useful common initial sequences,
it makes sense for each object to include pointers to an appropriate
TYPE_INFO object, so code that knows that it has e.g. a pointer to an
ADVANCEDFOO or derivative thereof can access members of its typ that are
unique to that type.

If that is not possible, using a void* will make it possible for code to
get by with a compiler-checked type coercion from void* to another type,
rather than having to use a cast.

David Kleinecke

unread,
Jun 15, 2017, 10:44:31 PM6/15/17
to
I belong to the "let them shoot themselves in the foot" school.
But I think this is not the right moment to discuss that
off-hand suggestion of mine.

David Kleinecke

unread,
Jun 15, 2017, 10:45:32 PM6/15/17
to
Looks interesting to me. Are you aware of GObject version
of OOPS in C?

Obviously there are difference ways to do it. I wanted to
"document" my version somewhere where I could get at least
a little commentary.

Chris M. Thomasson

unread,
Jun 15, 2017, 11:03:43 PM6/15/17
to
On 6/12/2017 10:51 AM, David Kleinecke wrote:
> C is not an object oriented language (see C++). But object-
> orientation is an organizing philosophy and a set of rules.
> In OO organization the focus is on the data - rather than
> the code. In classic C it was not quite clear where the
> persistent data lived. One reading would be that it lived
> in the set of top-level declarations in a translation unit
> (and in static declarations in functions). But that reading
> remains unsatisfactory because it cannot handle more than
> one set of data at a time.
>
> Suppose I wish to simultaneously run two instances of the
> same program with different data. Unless I take special pains
> I must load the code for the program twice (for well-known
> reasons). The special pains I must take can be object
> orientation.
[...]

Fwiw, here is a little minimalist OOP technique I use in C from time to
time:

https://pastebin.com/raw/f52a443b1

works pretty good.

David Brown

unread,
Jun 16, 2017, 7:36:18 AM6/16/17
to
I belong to the "don't give a blind man a machine gun and yell 'aim over
there' at him" school.

It is fair enough to want to avoid overhead or safety checks that hinder
flexibility or have run-time costs. But the type checks that you get
from putting things in structs are free - and when you are using macros
and functions around the accesses anyway, they also have zero cost in
programmer effort or the legibility of the source code. You have to be
desperate to be sent home on injury leave to use void* under these
circumstances.

Mr. Man-wai Chang

unread,
Jun 16, 2017, 8:00:50 AM6/16/17
to
On 13/6/2017 1:51 AM, David Kleinecke wrote:
> C is not an object oriented language (see C++). But object-
> orientation is an organizing philosophy and a set of rules.
> In OO organization the focus is on the data - rather than
> the code. In classic C it was not quite clear where the
> persistent data lived. One reading would be that it lived
> in the set of top-level declarations in a translation unit
> (and in static declarations in functions). But that reading
> remains unsatisfactory because it cannot handle more than
> one set of data at a time....

You can always implement object-orientation in a C program WITHOUT
object-oriented syntax or C++ pre-processors. And there are
object-oriented programs that're hard to understand than well-written C
programs. :)


--
@~@ Remain silent! Drink, Blink, Stretch! Live long and prosper!!
/ v \ Simplicity is Beauty!
/( _ )\ May the Force and farces be with you!
^ ^ (x86_64 Ubuntu 9.10) Linux 2.6.39.3
不借貸! 不詐騙! 不援交! 不打交! 不打劫! 不自殺! 請考慮綜援 (CSSA):
http://www.swd.gov.hk/tc/index/site_pubsvc/page_socsecu/sub_addressesa

supe...@casperkitty.com

unread,
Jun 16, 2017, 1:24:26 PM6/16/17
to
On Friday, June 16, 2017 at 6:36:18 AM UTC-5, David Brown wrote:
> It is fair enough to want to avoid overhead or safety checks that hinder
> flexibility or have run-time costs. But the type checks that you get
> from putting things in structs are free - and when you are using macros
> and functions around the accesses anyway, they also have zero cost in
> programmer effort or the legibility of the source code. You have to be
> desperate to be sent home on injury leave to use void* under these
> circumstances.

If C had a way of indicating that compilers should recognize certain kinds
of compatibility (not necessarily transitive) among different pointer types,
there would be far less need to use void*. It would be especially useful
if there were a means of declaring that a particular type T should be
implicitly convertible to U, pointers of type U should be presumed capable
of altering objects of type T, and that for purposes of the Common Initial
Sequence rule a pointer to a T should be considered equivalent to a pointer
to a U.

Because C does not have such a feature, use of types other than void* can
create a lot of headaches.

David Kleinecke

unread,
Jun 16, 2017, 2:59:55 PM6/16/17
to
Oh sure. It it foolish to edict free checks out of
existence. I can remember thinking once that the C
standard was forcing me to do just that. And I remember
that, on that occasion, the standard was right and I
was wrong. But I don't remember the technical point
involved. But since then my position has been: Do as
much as the standard requires - and nothing more.

And, of course, I think every deviation from the
standard merits at least a warning. I suppose I
should also require warnings if any free additional
checks fail.

Thiago Adams

unread,
Jun 17, 2017, 11:24:22 PM6/17/17
to
I think the patterns you suggest are at superficial level of style.
Style is important for company guidelines, but my point is that we need to search deeply.

I use some
patterns in my code and I can identify similar patterns in other people's code. I also find some code in C very different from mine.

One sample I have is the concept of "clear" meaning destroy.

Some people do this

X_Init
X_Clear

X_Clear is the function that you must call at end of X live-time.
But you also can call clear to put X at the initial state.

I don't mix these two concepts. I use X_Destroy.
After X_Destroy I cannot use X anymore. X_Destroy is more efficient also
because it don´t put variables at initial state when the object will not be used anymore.
X_Destroy it is not only a naming convention that I am describing but a concept of operation.

The destroy operation is the final function called when you have finalized the use of some data type/object.The object passed must be initialized.

The other concept I have is initialization.
My initialization concept puts the data in a state that can be destroyed.
this state can be intermediary "stand by" but generally this initial state works well for "empty state" as well.
There is one uninitialized state that must be know at compile time.

The destroy concept is similar of C++ destructor but not identical.
In C++ you cannot ignore or turn off the destructor /constructor by instance, just by type.
In C++ you cannot specify the destructor for a type*. My concept of destroy
is something applied for the declarator(variable)including pointers.

I have more patterns and I am thinking in transform them in operators in a C extended language.

Some of them:

new, delete
initialization {}, destroy
swap

Like C++, these operators needs to have ways of customization.

Öö Tiib

unread,
Jun 18, 2017, 8:55:52 AM6/18/17
to
On Sunday, 18 June 2017 06:24:22 UTC+3, Thiago Adams wrote:
>
> The destroy concept is similar of C++ destructor but not identical.
> In C++ you cannot ignore or turn off the destructor /constructor by
> instance, just by type.

In C++ it is technically achievable but will cause undefined behavior
when the class is not "standard layout".

> In C++ you cannot specify the destructor for a type*. My concept of destroy
> is something applied for the declarator(variable)including pointers.

In C++ we need struct (or class or union) for to add destructor and
constructor to type. Adding that struct is no problem since there are
no efficiency differences if to use a struct that contains only one
pointer member or that pointer type directly.

Thiago Adams

unread,
Jun 18, 2017, 3:33:17 PM6/18/17
to
On Sunday, June 18, 2017 at 9:55:52 AM UTC-3, Öö Tiib wrote:
> On Sunday, 18 June 2017 06:24:22 UTC+3, Thiago Adams wrote:
> >
> > The destroy concept is similar of C++ destructor but not identical.
> > In C++ you cannot ignore or turn off the destructor /constructor by
> > instance, just by type.
>
> In C++ it is technically achievable but will cause undefined behavior
> when the class is not "standard layout".

How?
I am not sure if we are talking about the same thing.


struct Point {
Point() { printf("ctor"); }
~Point() { printf("dtor"); }
};
int main()
{
Point pt;
return 0;
}
How to disable destructor for variable pt?

>
> > In C++ you cannot specify the destructor for a type*. My concept of destroy
> > is something applied for the declarator(variable)including pointers.
>
> In C++ we need struct (or class or union) for to add destructor and
> constructor to type. Adding that struct is no problem since there are
> no efficiency differences if to use a struct that contains only one
> pointer member or that pointer type directly.

Take the smart pointer shared_ptr.


struct Base { int x; };

struct Derived : Base { int y; };

void F(const std::shared_ptr<Base>& sp) {
printf("%d", (int)sp.use_count());
}

int main()
{
std::shared_ptr<Derived> spDerived = std::make_shared<Derived>();
F(spDerived);
return 0;
}


When we cast shared_ptr<Derived> to shared_ptr<Base> to call F, a new smart pointer of type shared_ptr<Base> is created. The reference counter is incremented, consequently we have performance penalty.
C++ templates creates a completely new type for the object. The same if you cast to shared_ptr<const Derived>.






Pascal J. Bourguignon

unread,
Jun 18, 2017, 4:59:57 PM6/18/17
to
r...@zedat.fu-berlin.de (Stefan Ram) writes:

> Thiago Adams <thiago...@gmail.com> writes:
>>Point pt;
>>return 0;
>>How to disable destructor for variable pt?
>
> Replace »return 0;« by »exit( 0 );«.

Well, no. exit() will call the atexit() functions, and AFAIK, the C++
runtime can make destructors be called from there (or even, directly
from the main function, given that exit() is declared noreturn).

I would use _exit() to ensure no destructor is called.

Öö Tiib

unread,
Jun 19, 2017, 10:06:52 AM6/19/17
to
On Sunday, 18 June 2017 22:33:17 UTC+3, Thiago Adams wrote:
> On Sunday, June 18, 2017 at 9:55:52 AM UTC-3, Öö Tiib wrote:
> > On Sunday, 18 June 2017 06:24:22 UTC+3, Thiago Adams wrote:
> > >
> > > The destroy concept is similar of C++ destructor but not identical.
> > > In C++ you cannot ignore or turn off the destructor /constructor by
> > > instance, just by type.
> >
> > In C++ it is technically achievable but will cause undefined behavior
> > when the class is not "standard layout".
>
> How?
> I am not sure if we are talking about the same thing.
>
>
> struct Point {
> Point() { printf("ctor"); }
> ~Point() { printf("dtor"); }
> };
> int main()
> {
> Point pt;
> return 0;
> }
> How to disable destructor for variable pt?

When we want to manage construction and destruction manually
in C++ then we have to build the object into some buffer for
example one got from 'malloc', or into 'std::aligned_storage'
or into some char array:

#include <stdlib.h>
#include <stdio.h>
#include <new>

struct Point {
Point() { printf("ctor"); }
~Point() { printf("dtor"); }
};

int main() {
alignas(alignof(Point)) char data[sizeof(Point)]; // make buffer

Point* p = new (data) Point(); // construct

// use our point

p->~Point(); // destruct
}

That is then the usage like in C with full control.
Since it is standard layout class the construction can be omitted
(by replacing it with something like "Point* p = (Point*)data;")
and result is then uninitialized object with uninitialized members.

>
> >
> > > In C++ you cannot specify the destructor for a type*. My concept of destroy
> > > is something applied for the declarator(variable)including pointers.
> >
> > In C++ we need struct (or class or union) for to add destructor and
> > constructor to type. Adding that struct is no problem since there are
> > no efficiency differences if to use a struct that contains only one
> > pointer member or that pointer type directly.
>
> Take the smart pointer shared_ptr.
>
>
> struct Base { int x; };
>
> struct Derived : Base { int y; };
>
> void F(const std::shared_ptr<Base>& sp) {
> printf("%d", (int)sp.use_count());
> }
>
> int main()
> {
> std::shared_ptr<Derived> spDerived = std::make_shared<Derived>();
> F(spDerived);
> return 0;
> }
>
>
> When we cast shared_ptr<Derived> to shared_ptr<Base> to call F, a new smart pointer of type shared_ptr<Base> is created. The reference counter is incremented, consequently we have performance penalty.
> C++ templates creates a completely new type for the object. The same if you cast to shared_ptr<const Derived>.

Yes, "struct X{Derived*p;}" can't be cast into "struct Y{Base*p;}"
despite "Derived*" can be cast into "Base*". We have to write
little code that does that conversion if we need it.

The real issue with "std::shared_ptr" is that it not too cheap. Therefore
when we have no need for shared ownership (or other reason for
reference counting) and performance matters then we should prefer
something cheaper like "std::unique_ptr" that performs about as fast
as a raw pointer.

Also when we do not need the "optional" feature of smart pointer (that
it can be empty) then we can pass reference to object itself, not
reference to pointer pointing at it. Again cheaper.

Thiago Adams

unread,
Jun 20, 2017, 9:51:39 AM6/20/17
to
On Monday, June 19, 2017 at 11:06:52 AM UTC-3, Öö Tiib wrote:
> On Sunday, 18 June 2017 22:33:17 UTC+3, Thiago Adams wrote:
> > On Sunday, June 18, 2017 at 9:55:52 AM UTC-3, Öö Tiib wrote:
> > > On Sunday, 18 June 2017 06:24:22 UTC+3, Thiago Adams wrote:

[...]

> > >
> > > > In C++ you cannot specify the destructor for a type*. My concept of destroy
> > > > is something applied for the declarator(variable)including pointers.
> > >
> > > In C++ we need struct (or class or union) for to add destructor and
> > > constructor to type. Adding that struct is no problem since there are
> > > no efficiency differences if to use a struct that contains only one
> > > pointer member or that pointer type directly.
> >
> > Take the smart pointer shared_ptr.
> >
> >
> > struct Base { int x; };
> >
> > struct Derived : Base { int y; };
> >
> > void F(const std::shared_ptr<Base>& sp) {
> > printf("%d", (int)sp.use_count());
> > }
> >
> > int main()
> > {
> > std::shared_ptr<Derived> spDerived = std::make_shared<Derived>();
> > F(spDerived);
> > return 0;
> > }
> >
> >
> > When we cast shared_ptr<Derived> to shared_ptr<Base> to call F, a new smart pointer of type shared_ptr<Base> is created. The reference counter is incremented, consequently we have performance penalty.
> > C++ templates creates a completely new type for the object. The same if you cast to shared_ptr<const Derived>.
>
> Yes, "struct X{Derived*p;}" can't be cast into "struct Y{Base*p;}"
> despite "Derived*" can be cast into "Base*". We have to write
> little code that does that conversion if we need it.

Exactly, but the conversions are not perfect and not solve problems
caused on the type system.

Sample:

struct Base
{
virtual Base* Clone() { return new Base(); }
virtual std::unique_ptr<Base> Clone2() { return std::make_unique<Base>(); }
};

struct Derived : Base
{
virtual Derived* Clone() { return new Derived(); }
virtual std::unique_ptr<Derived> Clone2() { return std::make_unique<Derived>(); }
};

//error C2555: 'Derived::Clone2':
//overriding virtual function return type differs and is
//not covariant from 'Base::Clone2'

Probably there are much more problems and situations where we need
create two versions of the same function or do more casts etc.

Öö Tiib

unread,
Jun 22, 2017, 5:01:31 AM6/22/17
to
Yes, covariance is sometimes useful convenience feature.
If the effect of covariant return type is useful then we have to
return raw pointers or to live without that feature.


>
> Probably there are much more problems and situations where we need
> create two versions of the same function or do more casts etc.

I haven't seen that as problem. Only little inconvenience. Covariant
return type provides no performance benefits or something that really
matters. The actual problems are elsewhere. For example if one
forgets to add that Clone override to one of derived classes then
he will get sliced clone. That will likely hurt.
0 new messages