MLRelated.com
Blogs

C to C++: Bridging the Gap from C Structures to Classes

Jacob BeningoMay 23, 20238 comments

In our last post, C to C++: Proven Techniques for Embedded Systems Transformation, we started to discuss the different ways that C++ can be used to write embedded software. You saw that there is no reason to be overwhelmed by trying to adopt complex topics like metaprogramming out of the gate. An important concept to understand is that you can make the transition gradually into C++ while still receiving the many benefits that C++ has to offer.

One of the first concepts that a C programmer needs to grasp is what objects are, how to define them, and the differences between objects in C and C++. (Yes, you can create objects in C!). In this post, I’m going to walk you through the foundational concepts of what objects are and show you how we bridge the gap from C structures to C++ classes. Buckle your seatbelt, this should be fun!

What is an object?

In object-oriented programming (OOP), an object represents a combination of data and functions that manipulate that data (often referred to as behaviors). Objects are typically defined through a class or structure, which outlines the attributes (data) and methods (functions). It's important to note that the class or structure itself is not an object but rather a blueprint. The actual object is created when the blueprint is used to allocate memory and initialize the object. Let’s look at a brief example.

If you are working on an embedded system, the chances are high that you will have an LED in your system. In software, we might have an object named ledGreen that is instantiated, allocated and initialized, from a led class. That class might contain attributes about the led such as its state, on or off. Our class would also have methods associated with it that allow us to act on that state data such as on(), off(), and maybe even toggle().

That’s it! The general idea behind objects is quite simple, it’s the details on how we create the specifications where things can get tricky. Since you are probably a C programmer, let’s look at how we would define an object in C.

Using C structures to create objects

The most natural mechanism in C to create an object is to use a structure. A structure is a user-defined datatype that allows the programmer to group related data together. For example, an LED object might have attributes like state, brightness, and so forth. An example C struct definition might look like the following:

typedef struct {
   bool      isOn;
   uint8_t   brightness;
}Led_t;

You could define a greenLED object using C code like the following:

Led_t greenLed = {false, 0};

That’s a super simple LED object that doesn’t have any methods. If we wanted to manipulate the object, we would have to write functions that we pass the object into that would then operate on the data. The approach is very procedural because we have our data and methods separate from each other, where an OOP approach would package these together.

We can take an interesting approach of using function pointers to start to package our data and methods together. For example, if our Led_t will have on and off methods, you could write the struct as follows:

typedef struct {
   bool      isOn;
   uint8_t   brightness;
   void      (*on)(void);
   void      (*off)(void);
}Led_t;

As you can see, we now have a pointer to a function that is packaged with our Led_t. If we were to instantiate this struct, we would have to specify what function we want to use to manage the on and off behavior of the LED:

Led_t greenLed = {false, 0, LedOn, LedOff}; 

Somewhere we would have functions LedOn and LedOff defined like: 

void LedOn (Led_t * const led) {
       led->isOn = true;
       led->brightness = 255; 
} 

void LedOff (Led_t * const led) {
       led->isOn = false;
       led->brightness = 0; 
}

With the C code we’ve been exploring, you could then turn on a led on, off, or change the brightness using syntax like:

greenLed.on();
greenLed.off();
greenLed.on();
greenLed.brightness = 128;

Using C++ structures to create objects

We can extend our knowledge of C structures to define an object in C++ using structures too. In C, all our data is public and can be accessed by anyone who has access to our object. This isn’t that great is we want to leverage encapsulation and data hiding. In C++, we have the tools to do this within a structure. First, let’s look at how I might change my C structure into a C++ structure. The code can be seen below:

struct Led_t {
   bool isOn;
   uint8_t brightness;

   void on() {
      // Implementation of the 'on' function
      // ...
   }

   void off() {
      // Implementation of the 'off' function
      // ...
   }
};

Notice a few minor changes here:

  1. You no longer need to typedef the struct. In C++, a struct def is a type and we create an instance of it when we define a variable like: Led_t greenLed;
  2. A struct in C++ automatically makes its members public. You’ll see shortly that we can use the private and public keywords to control access.
  3. The implementation for our on and off functions can be implemented directly in our struct! They could also be defined in a separate source file if it makes our definition too complex. In that case, we’d use the scope resolution operator with code like the following:
void Led_t::on() {
    // Implementation of the ‘on’ function
    //  . . .
}

These are some minor but very powerful differences between C and C++. For example, we discussed how in C our data members are public. By default, they are also public in C++; however, we can utilize data hiding by using the private and public keywords. Take a moment to look at this updated version of code:

struct Led_t {
private:
   bool isOn;
   uint8_t brightness;

public:
   void on();
   void off();
};

void Led_t::on() {
   // Implementation of the 'on' function
   // ...
}

void Led_t::off() {
   // Implementation of the 'off' function
   // ...
}

The public members are our methods which are used by the application to get the behavior that is needed from the Led_t objects. Our data is hidden, so that no one can write code like:

ledGreen.isOn = true;

You will find that some developers prefer to use struct instead of a C++ class because of it defaults to public for its members. Their code can be simplified to not include the public keyword as follows:

struct Led_t {
   void on();
   void off();

private:
   bool isOn;
   uint8_t brightness;
};

Using C++ classes to create objects

With your understanding of C++ structs, the transition to using C++ classes will be quite simple. The difference between a C++ struct and class is the following:

  • A struct by default has public members and public inheritance
  • A class by default has private members and private inheritance

In modern C++, the choice between using a struct or a class is more coding style and personal preference. When I first learned C++ in the late 90’s, we only used struct for simple data types. Today, though, classes and struct are nearly identical except for those minor differences that I described above.

You might be wondering how a C++ class might implement the Led_t; below is an example class definition:

class Led_t {
public:
   Led_t();
   void on();
   void off();

private:
   bool isOn;
   uint8_t brightness;
};

The above code should look super familiar to you. There are only a few differences:

  1. We have changed the struct keyword to a class keyword
  2. We’ve added a constructor, Led_t, that will initialize our object. In fact, you can add constructors to a struct too! I just didn’t do so in previous examples to slowly warm you up to the idea.

Constructors are special methods of a class or struct that are automatically called when an object is instantiated. You generally use a constructor to initialize the object. One cool thing about constructors is that you can have multiple constructors will different method signatures that overload each other. Then when you instantiate the object, the signature you use will determine which constructor is executed. The details of this are beyond today’s blog, but we’ll touch on this in the future.

Conclusions

The idea of moving away from C to C++ can be intimidating. C++ is a rich and powerful language. I hope you’ve realized from today’s post that many concepts you already understand in C still exist in C++. In fact, they’ve just evolved and have become more powerful over the decades. We’ve explored what a C object looks like, how that can be translated into a C++ struct, and then finally the differences between C++ structs and classes. You should now be armed with some fundamental knowledge that should help you to start writing your own objects in C++. In the next post, I’ll show you several tips for refactoring and porting your existing C code into C++.




[ - ]
Comment by fjrg76June 1, 2023

Hi Jacob, your C code doesn't compile. 

I was just about to point you out a better class C implementation when I noticed some issues with your code, so I tried it. 

The thing that I don't like about your implementation is that every Led_T object must allocate memory for the address of the binded functions.

So, every object is allocating  24 bytes instead of only 2 bytes (bool, uint8_t):

typedef struct Led_T
{
   bool isOn;
   uint8_t brightness;

   void (*on)( struct Led_T* const );
   void (*off)( struct Led_T* const );
} Led_T;

As this output shows:

$ ./a.out 
Sizeof( Led_T ) = 24

(I'm compiling in a 64 bit box, but the fact that every object is using more memory than needed holds.)

One cannot use this implementation in memory-constrained microcontrollers. 

Even worse, imagine that you want to declare two objects:

Led_t greenLed = {false, 0, LedOn, LedOff};
Led_t redLed =   {false, 0, LedOn, LedOff};

You need to pass over and over on the address of the binded functions. What about an array of LED's?

Another thing in your code is that if you want to use these functions:

LedOn( Led_t * const led) {}
LedOff(Led_t * const led) {}

Then you need to modify the methods' signatures, as I did and you can see. Your proposal are functions without arguments, but that doesn't work.

Even doing such modification your code still doesn't compile because you can't use these statements:

greenLed.on();
greenLed.off();

Did I miss something? I'll show you my example of a better implementation in another "Comment" post.


[ - ]
Comment by beningjwJune 1, 2023

Thanks for the comment and questions. The code that is in the blog is not intended to be compilable per say, but to convey concepts that lead the reader from a C struct, which should be common to them to C++ classes. They should then be able to go and write their own code using the concepts. I've glossed over a lot of the C details in order to get to the C++ pieces. 

If I were to throw this code into an actual module for testing the concepts, it would look something like the following:

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stddef.h>

typedef struct {
   bool      isOn;
   uint8_t   brightness;
   void      (*on)(void);
   void      (*off)(void);
}Led_t;

void led_on(void);
void led_off(void);

Led_t led = {
   .isOn = false,
   .brightness = 0,
   .on = led_on,
   .off = led_off
};

void led_on(void) {
   led.isOn = true;
   led.brightness = 100;

   printf("Led is on!\n");
}

void led_off(void) {
   led.isOn = false;
   led.brightness = 0;

   printf("Led is off!\n");
}

int main(void) {
   led.on();
   led.off();

   printf("Led_t size is %lu\n", sizeof(Led_t));

   return 0;
}

The above code would not be production intent, but it gives some ideas on how you might create an interface in C. The goal of this series though isn't to write these things in C, but to move over to C++.

If I compile the above code, I get the following output from the program:

Led is on!
Led is off!
Led_t size is 24

A developer could easily change what their on off functions are and compile for either a host environment or a target environment. 

As for the sizeof(Led_t), on a 64-bit machine I would expect to get something around 24 bytes. You should get something like:

isOn: 1 byte
brightness: 1 byte
on function pointer: 8 bytes
off function pointer: 8 bytes

You could actually check this by updating the above main function to the following to get the layout of the Led_t struct (note that you'll need to include stddef.h:

int main() {
    printf("Size of Led_t: %zu\n", sizeof(Led_t));
    printf("Offset of isOn: %zu\n", offsetof(Led_t, isOn));
    printf("Offset of brightness: %zu\n", offsetof(Led_t, brightness));
    printf("Offset of on: %zu\n", offsetof(Led_t, on));
    printf("Offset of off: %zu\n", offsetof(Led_t, off));

    return 0;
}

Running this code to get the offsets results in the following output:

Size of Led_t: 24
Offset of isOn: 0
Offset of brightness: 1
Offset of on: 8
Offset of off: 16

As you can see, there is padding added by the compiler between the brightness variable and the on function pointer. Most likely this is for byte alignment and to improve execution efficiency. 

On an embedded target, the pointers are typically 4 bytes and if you pack the structure then you'll get a sizeof(Led_t) closer to 10 bytes which is a small price to pay to decouple code through an interface. 

For any production code, my test code would need to be cleaned up a bit, but I hope the concepts anyways make sense. 

[ - ]
Comment by fjrg76June 1, 2023

Hi!

The thing is not about where those 24 bytes come from, but the object's size must be 2 bytes (for this example).

In the example you've declared 1 object (24 bytes); for a single object that could work. However, now imagine that you need 2 (48 bytes) of them, or 3 (72 bytes), or an array (24*n bytes); such approach isn't the best.

I've always used this other approach, more verbose, but come'on, we are in C. In it every object only uses as much memory as strictly needed (besides padding):


typedef struct
{
   uint8_t isOn;
   uint8_t brightness;
} Led_T;

void Led_Init( Led_T* this )
{
   this->isOn = false;
   this->brightness = 0;
}

void Led_Off( Led_T* this )
{
   this->isOn = false;
   this->brightness = 0;
}

void Led_On( Led_T* this )
{
   this->isOn = true;
   this->brightness = 127;
}

bool Led_IsOn( const Led_T* this )
{
   return this->isOn;
}

int main()
{
   Led_T greenLed;            // Only 2 bytes!

   Led_Init( &greenLed );     // Ctor like

   if( ! Led_IsOn( &greenLed ) )
   {
      Led_On( &greenLed );
   }

   Led_T array_of_Leds[1000]; // only 2000 bytes!

   // ...

   for( int i = 0; i < 1000; ++i )
   {
      Led_On( &array_of_Leds[ i ] );
      // turn on all LEDs!
   }

   printf( "Sizeof( Led_T ) = %ld\n", sizeof( Led_T ) );
   printf( "Sizeof( greenLed ) = %ld\n", sizeof( greenLed ) );
   printf( "Sizeof( array_of_Leds[1000] ) = %ld\n", sizeof( array_of_Leds ) );
}

// Output
$ ./a.out 
Sizeof( Led_T ) = 2
Sizeof( greenLed ) = 2
Sizeof( array_of_Leds[1000] ) = 2000


And yes, sometimes I miss the cleaner and concise C++ syntax when coding objects in C. Nevertheless, this approach gets the job done and I think it pretty much imitates the way C++ works internally.

Now, if your approach is going to be used only for interfaces and decoupling the logic from the hardware, then I do agree with you because I would do the same. However, I think you don't mention interfaces in this article =)


Waiting for your next book, greetings!



[ - ]
Comment by beningjwJune 2, 2023

Ah, I understand now, I think. I write about 12 blogs per month and develop several days of course material so sometimes need to reread to remember what I was getting at. 

For this blog, we aren't focused on how to write efficient object-oriented C code. I'm trying to take a C programmer (the target audience) and build up the concepts and relationship of a C struct to C++ structs and classes. 

In this series we've defined that an object puts data and operations together. So the C representation of that is what I've put the post. It's not necessarily the most efficient or correct C code, but the reader should be able to draw the line between that implementation and the organization of the C++ struct and class. 

I've also written Led_t in this way to act as an interface so that I could redirect an Led_t object to either interact with LED hardware, or a simulated LED, but that is beyond the scope of what I was trying to do in this blog. So, it's a "hidden" intent. 

I do agree with you that the C implementation could be done differently if we were concerned with the size of the Led_t object. The implementation you've shared definitely does the trick, and is similar to a strategy I have used in the past. 

I really appreciate the question(s), comments, and sharing of code. I think it's really constructive and helpful to other readers! 

(My latest book, Embedded Software Design, was released November 2022. I probably won't start writing the next one until sometime this Fall. I have several ideas I'm trying to choose between). 

[ - ]
Comment by dcomerJune 8, 2023

As always, Jacob, A GREAT post! Thanks.

[ - ]
Comment by beningjwJune 8, 2023

Thanks!

[ - ]
Comment by omarmhJuly 4, 2023

It was always in front of me and I had trouble understanding

but now it's clear thanks Jacob..

[ - ]
Comment by beningjwJuly 4, 2023

You’re welcome

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: