Home > On-Demand Archives > Talks >

OOP in C

Nathan Jones - Watch Now - EOC 2023 - Duration: 01:00:38

OOP in C
Nathan Jones
Believe it or not, it's possible (and encouraged!) to use object-oriented programming (OOP) in plain-old C. Object-oriented code exhibits the qualities of abstraction, encapsulation, polymorphism, and inheritance, and these qualities can help make an application easier to write, debug, and change. In this talk, I'll demonstrate how to do achieve these qualities in C, ranging from the simple to the complex. This talk is based entirely on work I share publicly here: https://github.com/nathancharlesjones/Comparison-of-OOP-techniques-in-C.
M↓ MARKDOWN HELP
italicssurround text with
*asterisks*
boldsurround text with
**two asterisks**
hyperlink
[hyperlink](https://example.com)
or just a bare URL
code
surround text with
`backticks`
strikethroughsurround text with
~~two tilde characters~~
quote
prefix with
>

Irshadjs
Score: 0 | 7 months ago | no reply

Thanks Nathan, this was wonderful.

arjunvinod
Score: 0 | 12 months ago | no reply

Thank you for an amazing presentation, Nathan. Very informative.

DS
Score: 0 | 1 year ago | no reply

Fantastic material and presentation! I really enjoy and appreciate seeing such a relatively clean implementation of OOP techniques in C!

viewer
Score: 0 | 1 year ago | 1 reply

Good job. A slightly different way than I use now. It is good to see different ways of implementing OOP. I think yours looks more like cpp. I tend to use build time memory allocation is set and immutable. It is nice to have another tool in the toolkit.
FYI: Would be nice if it was not just coming in on left speaker. (right channel had no sound).

nathancharlesjonesSpeaker
Score: 0 | 1 year ago | 1 reply

Thanks for the feedback! I'll check my audio next time. :)
What do you mean by "build time memory allocation is set and immutable"? I don't think I understand.

viewer
Score: 0 | 1 year ago | 1 reply

Because I write for embedded system where many things are know at build time. I also want an easy way to configure many different instances of an object differently for different projects. In this way the _cfg.h file is configuration that easily way of configuring
many instances of an object
The .c file should not call out any one object, unless all will have it.
As you can see the ducks always present from boot.
There is no constructor or deconstructor.
It is less on the fly configurable.
Sometimes it would be nice to have more configuration on the fly,
which is why i like your implementation style.
I also like your pointer to function syntax rather than straight functions.
I wrote this, did compile it but I think it is enough to give you an idea

/ Start duck_cfg.h/
/ this is a configuration of ducks that don't change/
DUCK(HUEY, red, DUCK_MALLARD, 97)
DUCK(DEWEY, blue, DUCK_RUBBER, 98)
DUCK(LOUIE, green, DUCK_NONE, 96)
/ End duck_cfg.h/

/ start duck.h /

ifndef DUCK_H

define DUCK_H

typedef enum {

define DUCK(name, ...) name,

include "duck_cfg.h"

NUM_DUCK

undef DUCK

}duck_e;
...
extern uint32_t DUCK_Init(void);
extern uint32_t DUCK_GetAge(duck_e key);
extern uint32_t DUCK_GetType(duck_e key);
extern uint32_t DUCK_Process(void);

...

endif / DUCK_H /

/ end duck.h /

/ start duck.c /
typedef enum
{
red,
blue,
green,
}duck_color_e;

typedef enum
{
DUCK_MALLARD,
DUCK_RUBBER
DUCK_NONE,
}duck_subType;

typedef struct {
duck_color_e color;
duck_subType type;
uint32 temperatureInit;
}duck_cfg_t;

/ changeable private var/
typedef struct {
uint32_t age;
uint32_t temperature;
}duck_t;

const duck_cfg_t duckCfg[NUM_DUCK] = {

define DUCK(name, color, type, temperatureInit) \

{color, type, temperatureInit},

include "duck_cfg.h"

undef DUCK

};

duck_t duck[NUM_DUCK] = {};

static uint32_t DUCK_GetTemperature(duck_e key);

uint32_t DUCK_Init(void)
{
duck_e i;
for(i = (duck_e) 0; i < NUM_DUCK; i++)
{
/ do something for each duct to init/
duck[i].temperature = duckCfg[i].temperature;
duck[i].age = 85;
}
}

uint32_t DUCK_GetAge(duck_e key)
{
ASSERT(key < NUM_DUCK);
return duck[key].age;
}

uint32_t DUCK_GetType(duck_e key)
{
ASSERT(key < NUM_DUCK);
return duckCfg[key].type;
}

uint32_t DUCK_Process(void)
{
duck_e i;
for(i = (duck_e) 0; i < NUM_DUCK; i++)
{
/ do something for each duct /
DUCK_GetTemperature(i);
}
}

/ Private /
static uint32_t DUCK_GetTemperature(duck_e key)
{
/ get temp for that duck /
}

nathancharlesjonesSpeaker
Score: 1 | 1 year ago | no reply

Okay, I think I understand. You're right, that's an option I hadn't really addressed in my presentation! The programmer is allowed to configure the desired number of objects at compile-time but no more are allowed to be made at run-time because there is no "create" function. The end results seems very similar to using the "opaque struct" with a static memory pool but some folks might appreciate being able to configure those objects at compile-time (like you show here) as opposed to run-time (which is what one would have to do if they were following my code). I'll have to add this to my notes!

srikrishnachaitanya
Score: 0 | 1 year ago | 1 reply

Fantastic video Nathan!
I loved the idea of the nesting doll structures and magic numbers!

nathancharlesjonesSpeaker
Score: 0 | 1 year ago | no reply

Thanks so much! I shamelessly stole that idea from Axel Schreiner. I really appreciate your feedback!

nathancharlesjonesSpeaker
Score: 0 | 1 year ago | no reply

Hey, folks! I wanted to point out that I also answered a few questions that come up in the Live Q&A on the discussion thread associated with that video.

nathancharlesjonesSpeaker
Score: 0 | 1 year ago | no reply

Two great questions were asked in the Q&A today for which I wanted to give a more thorough answer. Here they are:

  1. Q: "How am I able to unit test a module if it's been abstracted (i.e. the internals are kept secret)?"
    There are two answers to this question, that I can tell: (1) "Don't" and (2) "Add some code that exposes the internals to a testing interface, but not to the rest of the application". (Elsewhere on the internet, Miro Samek seems to offer a third: "Use software tracing". He explained this technique in an earlier EOC talk, though I haven't watched it yet, myself.) The first answer, "Don't" means that if something has been hidden from the application, then the whole point is that we don't care about the implementation, just that the module adheres to the interface we've established in the header file. On the Embedded Artistry forum, Phillip Johnston also makes the great point that if you feel that you truly must test the internals of a module, then it's possible that your module has too many responsibilities and your design would be better served by dividing that module up, such that you can actually test, through a public interface, the things you want to verify. The second option, exposing parts of the module to strictly allow for unit testing, can be accomplished in a few ways:
    a. Use #ifdef...#endif to include/exclude the functions you need to test your module at build-time; the testing code includes something like #define TESTING while production code doesn't.
    b. Make WEAK functions that expose the internals of a module and then only provide implementations to the linker when compiling the testing code.
    c. Create derived classes for "production" and "testing". The "testing" derived class adds interface functions that expose the internals of the module.
    Although it's not clear to me if James Grenning explicitly answers this question in his book, "TDD in Embedded C", it seems to me that he employs both techniques throughout the book (in particular, the idea of creating a derived class that exposes certain details seems to be described in Section 11.5).

  2. Q: "When should I just use C++?"
    To (frustratingly) quote many engineers, "It depends." If a project requires C, then it's a bit of a moot point; ditto if a project requires certain language features that are only available in C++ such as templates, constexpr, or the auto keyword. If it's just preferred that a project use C, then there are design trade-offs to be made.
    a. The biggest consideration might just be how well folks understand C++ vs "OOP in C" ("folks" should include not just you, but also anybody else who will be writing, reviewing, using, and/or maintaining your code). Once you've implemented OOP in C a few times, things make more sense (maybe even more sense then what the C++ compiler is doing to implement a "non-virtual default destructor"!) and it's easier to stick with "OOP in C" for larger and more complex projects.
    b. Another consideration might be the overall size of the project. It takes a little more code to write "OOP in C" as opposed to implementing the same modules/classes in C++ since, essentially, we're having to create our own virtual tables. Accumulated over a very large project it's possible that writing all of that extra code takes too long for your liking.
    c. However, that little bit of extra code is sometimes a good thing! By implementing OOP yourself, you actually have very fine control over exactly how those OOP features are implemented. Constructors in C++ can't throw exceptions, for instance, but with OOP in C, you can employ any error handling technique you want in your constructors, even an approximation of try-except! (CException from Throw the Switch is one such implementation and there are many others.) Relatedly, I don't think C++ is needed to implement any of the classic Design Patterns or any complex software architectures that one might build using a traditional OOP language; OOP in C is perfectly capable of those things.
    d. Lastly, if inheritance is needed, I think C++ is definitely called for, unless you're willing to forego the idea of modules being opaque, which eliminates one very big problem. Creating inheritance in C (safely) while maintaining abstraction, though, is exceedingly difficult.
    In summary, I think "OOP in C" is as or more capable than C++ in implementing OOP designs (inheritance notwithstanding). It's a great fit if you want to stay in C or want a fine control over how OOP is implemented in your project and don't mind writing a little extra code for each of your modules. After you've played around with the techniques and feel like you understand them, then I think you'll find that OOP in C can solve many or all of your design problems. On the other hand, you'd probably want to use C++ if you need inheritance or other C++ language features or if you aren't yet comfortable using the OOP in C techniques.

JeanLabrosse
Score: 1 | 1 year ago | 1 reply

Very well done. I love the accelerated videos to get to what you are looking for :-)
In the encapsulation example, instead of iterating through the array to find 'unused' objects, I would have used a linked list of 'free' objects and then finding a free object is just the one at the head of the list.
I just question who encourages OOP using C?
Why not just use C++?

nathancharlesjonesSpeaker
Score: 1 | 1 year ago | no reply

Thanks, Jean! You're right that a linked list would be faster.
Regarding the purpose of "OOP in C", I think it's important to separate the concept of "OOP" into at least two parts: code that is modular and code that supports an inheritance structure. I would agree with you that if an inheritance structure is deemed necessary for a project, then C is not the right tool for the job. But all code benefits from being modularized well and it's those techniques, that subset of "OOP", which is incredibly useful for all programmers. (Miro Samek asked a great question in the Q&A which was (paraphrased) "How is it even possible to write a large application without good modularization?") Plus, I've found that with the run-time polymorphism technique I present I am able to implement all of the classic design patterns like Strategy, Factory, Template Method, etc. This is a boon for a developer who either chooses to work in C or is forced to by work requirements.

OUR SPONSORS

OUR PARTNERS