Follow US:

Practice English Speaking&Listening with: Embedded Programming Lesson 32: OOP-part4: Polymorphism in C

Normal
(0)
Difficulty: 0

Welcome to the Modern Embedded Systems Programming course.

My name is Miro Samek and in this lesson I'll continue

with OOP and polymorphism, but this time you will see how

to implement polymorphism in portable, standard-compliant

C code.

This should reinforce what you've learned about virtual

functions in C++ in the last lesson and expose some additional nuances

of the VPTR-VTABLE implementation.

This lesson will also provide some general principles

and guidelines about using object-oriented programming in embedded

software.

By the way, this is a very round lesson number hex-20, which

coincides with the even more round number of subscribers to the

Quantum Leaps video channel, which is about to cross hex-8000 or 2-

to-15-th.

I'd like to thank you all for your help in reaching these

fantastic milestones.

As usual, let's get started by copying the previous lesson30--the C

version without the underscore-CPP suffix, to lesson32.

Get inside the new lesson32 directory and double-click on the project

lesson to open it in the micro-Vision IDE.

To remind you quickly what happened so far, in the last lesson, you

learned about the OOP concept of polymorphism, which is the ability

to provide different methods for the same inherited operation in the

subclasses of a given class.

Specifically, you saw how polymorphism is implemented in C++ by means

of virtual functions and you have reverse-engineered the late-binding

mechanism, where the specific method for a given operation is

resolved at runtime based on the type of the object, not the type of

the pointer used in the call.

But before you go any further, from the last lesson is should be

clear that unlike class encapsulation and single inheritance, which

were essentially free in C, polymorphism in C will add coding

complexity and overhead.

Therefore, if you intend to use polymorphism extensively, you would be probably

better off by switching to C++.

However, if you build or use software libraries (such as the QP/C

real-time framework), the complexities of polymorphism in C can be

confined to the library and can be effectively hidden from the

application developers.

But either way, this lessons primary goal is to show you how things

really work under the hood, which should help you to use polymorphism

more efficiently and with greater confidence in any language.

So, today you will implement the virtual functions manually in C

following exactly the C++ VPTR-VTABLE design.

For this, you need to explicitly add the VPTR to the attribute

structure of the Shape base class.

The VPTR will be the first attribute, and will be a pointer to the

'const' ShapeVtable structure.

This 'const' will allow the VTABLE to reside in ROM.

Please note that at this point you have not provided the declaration

of the ShapeVtable struct just yet.

But here you are using only a *pointer* to this struct, which the compiler

accepts, because all it needs to know for processing the Shape attribute

structure is the size of the pointer, which is known, not the

size of the whole VTable.

But now, you obviously need to declare the VTABLE, which even though

it's called a "Table", is typically not implemented as an array but

rather as a structure of pointers to all *virtual* functions, such as

draw() and area() in the case of the Shape class.

I used pointers to functions in this video course before, but today

is the time to introduce them a bit more carefully.

So, the C language allows you to provide a pointer to a function,

just like you can provide a pointer to a variable.

In both cases, a pointer contains the address (of the function

or variable, respectively) and also the type information

about the entity that is being pointed to.

In case of pointers to functions, the type information consist of the

full signature of the function.

So, to declare a pointer to the draw() function, you first need to

write the signature of this function.

And then, you need to convert the actual name of the function into a

pointer, which means using the asterisk operator.

But because of the specific operator precedence rules in C, the

asterisk would be bound to the return value instead of the function

name,

so you need to add explicit parentheses around the pointer, like

this.

You apply the exact same steps to declare a pointer to the area()

function.

OK, so the VPTR and VTABLE are both declared, but now the users of

the Shape class as well as its subclasses must be able to call the

virtual functions.

You can provide this virtual call functionality, also known as late

binding, in a few different ways.

First, you can provide member functions for that, just like all the

other operations of the Shape class.

Specifically, you can take the draw() method signature and turn it

into the Shape_draw_vcall() function.

Similarly, you can take the area() method signature

and turn it into the Shape_area_vcall() member function.

The implementation of these virtual call functions goes to the

shape.c file.

Here, in the Shape_draw_vcall() function you first get from the "me"

pointer to the vptr.

Next, you get from the vptr to the specific pointer to function, such as draw.

And finally, you need to provide the parameters of the call, which include

the "me" pointer and potentially other parameters included in the

signature of the function.

You repeat the exact same steps for the Shape_area_vcall() function,

except that this function returns a value, so you need to add the

return statement in front.

As you can see, the C compiler accepts this syntax without

complaints, because the presence of the parameter list following the

pointer to function tells the compiler that this is a function call

based on this pointer-to-function.

However, this syntax fails to show you that you are de-referencing a

pointer and I prefer to be very explicit about it.

Therefore I prefer the alternative syntax analogous to de-referencing

any other pointer, that is, with the asterisk in front.

Of course, again, just like in the declaration of a pointer-to-function,

you need to use paretheses around the pointer.

As you see, the compiler accepts this version just as well.

But, either way, the most important point here is that the "me"

pointer is used *twice*: once to find the VPTR within the object, so

that the call is specific to the object, not to the type of theme

pointer; and the second time themepointer is used as the usual

first parameter of a member function.

So, this is the first clean and straightforward solution that will

work.

But it has a big drawback, and that is the additional function-

call overhead to call the invented vcall() functions.

But, the good news is that the newer C99 language standard allows you

to avoid this overhead, by using theinlinefunctions introduced

exactly for this purpose.

To make the functions inline, you move the whole definitions of the

functions into the header file and you add the keywords inline and

static in front.

The discussion as to why it is a good idea to use both inline and

static is a bit lengthy and Im going to leave it for another day, as

it is off topic for today.

But now, when you try to compile this code it fails, because as it

turns out, this compiler still works in the older C89 mode and does

not recognize theinlinekeyword.

You can remedy this, by opening the Project Options dialog box, and

going to the C/C++ tab, where you can tick the box next to C99 Mode.

As you can see, this triggers the complete re-compilation of the

whole project, but this time the code builds cleanly.

While the inline implementation of the virtual call mechanism is the

preferred way, for completeness Id like to mention the third

implementation option based on preprocessor macros, which will look

as follows:

You take the function definition and turn it into a macro by

stripping away most of the type information.

This is because macros provide only purely textual substitutions.

Therefore, to avoid any surprises, when for instance the client code

would use expressions for the macro parameters, its always a

good idea to enclose the macro parameters, like themepointer

here in additional set of parentheses.

For all these reasons, macros are not nearly as good as inline

functions.

But I still show you the macros, because they are the only

low-overhead option for the older C89 compilers, which are still in

extensive use.

Alright, so youve seen how to define and how to de-reference

pointers to functions, but you are not quite out of the woods yet

with your virtual functions in C.

You still need to somehow define the VTABLE in ROM and also you need

to setup the VPTR in each instance of the Shape class and its

subclasses.

As you remember from the reverse-engineered C++ implementation from

the last lesson, the VPTR and VTABLE setup occurs in the constructor,

so thats where you need to go next.

Here, you need to define an instance of the struct ShapeVtable in

ROM, meaning that it will be bothstatic�, that is not on the stack,

andconst�.

Now, aconstobject cannot be changed, so you must immediately

initialize at the point of creation.

But before you can initialize the vtable, you need to provide the

functions, with which to initialize the draw and area pointers-to-

functions in your VTABLE.

So, lets provide the prototypes for the functions: Shape_draw() and

Shape_area().

You can make these functions static, because they will be only used

locally inside the shape.c module.

So, now you can finally initialize your vtable instance, whereas you

can choose between the two options of the syntax.

First, you can directly use the function names.

The compiler will accept this, because the absence of parentheses

after the function names means that these are not function calls

but rather addresses of the functions.

However, I prefer the alternative syntax, in which you explicitly use

the ampersands, that is address-of operators, to leave absolutely no

doubt that here you mean addresses of the functions.

So, now, you can setup the vptr in the newly constructed Shape object

to point to the vtable for the Shape class.

And finally, you need to define the Shape_draw() and Shape_area()

functions.

Interestingly, at the level of the Shape class, you cannot really

provide meaningful implementations, because Shape is too abstract.

So, for now you just leave the functions empty, but to avoid compiler

warnings about unused parameters, you can use the idiom of casting

the unused parameter on void.

At this point, you are done with the general virtual function

machinery in the Shape base class, and you can immediately put it to

good use by writing the generic drawGraph() operation discussed in

the last lesson.

So, here is the C++ code from lesson 31, where an array graph[] holds

pointers to Shape, but these pointers actually point to subclasses of

Shape, such as Rectangle, Circle or Triangle.

The central point here is that this C++ code applies polymorphism to

call the specific draw() method for each Shape pointer in the graph

array in a very intuitive and elegant way.

So, now you your job is to do the same in C, using the virtual call

mechanism you just implemented.

Well, its actually very similar to the C++ version.

The only difference now is that you use...

the Shape_draw_vcall() inline function to apply polymorphism.

So, this is all there is to it, except that you obviously still need

to provide the prototype of the drawGraph() operation in the shape-

dot-h header file.

OK, the Shape base class is done, but some work is still needed in

the subclasses of Shape, like Rectangle or Circle.

They will all inherit the VPTR and the virtual call mechanism from

Shape.

But, each subclass, like Rectangle, still needs to provide its

own VTABLE and its own specific implementation of the virtual

functions draw() and area().

This was the main purpose of polymorphism.

So, lets do it now for the Rectangle subclass of Shape.

You need to go to rectangle-dot-c and pretty much repeat the steps

you did for the Shape base class.

So first, you add the static and const virtual table inside the

Rectangle constructor.

At this point, I presume that Rectangle does not add any new virtual

functions to the ones already specified in the Shape superclass.

In that case, Rectangle can use the ShapeVtable

structure directly.

Otherwise, you would need to apply inheritance in C with respect to

VTABLES, but lets keep it simple here and not add any new virtual

functions in Rectangle.

So, back to the initialization of the VTABLE, instead of Shapes

methods, the Rectangle VTABLE will obviously contain the Rectangles

implementations.

But now, Rectangle already provides its own Rectangle_draw() and

Rectangle_area() functions.

The problem is that themepointers in

the signatures of the Rectangle implementations are of type

Renctangle�,

while the pointers-to-functions in the Shapes VTABLE have signatures

that expectmepointers of type Shape.

Because remember, that even though Rectangle and Shape are related by

inheritance, the C compiler doesnt know about it and will not preform

upcasting automatically.

So, to make the Rectangle signatures fit into the VTABLE originally

defined for the Shape base class, you need to cast pointer-to-

functions, by using the whole function signatures, like this.

Only now, you can finally assign the inherited VPTR for the Rectangle

class, but you need to be very careful to do it *after* calling the

constructor of the Shape superclass.

This is because, just like in C+ +, the Shape constructor sets the VPTR to

point to the Shapes VTABLE, but here you want to override this

setting to the Rectangles VTABLE.

And this is all, because rectangle-dot-c already contains the

implementations of the Rectangle_draw() and Rectangle_area()

functions.

The very final touch, which will allow you to test all this in the

debugger, is to call to the generic drawGraph() operation that makes

use of the virtual call mechanism.

For this, you can open the C++ code from the last lesson and copy the

relevant snippet to your main-dot-c file.

In the C version, you need to setup the graph array of pointers

slightly differently, because you dont have the Circle class in C,

so lets use the s1 Shape object instead.

Also, instead of creating a Shape object dynamically, lets create a

Rectangle object.

Then, of course you need to call the right constructor on it as well.

So this is about it, but to make the C compiler happy, you still need

to add an explicit cast in the Rectangle constructor and in the

initialization of the graph[] array of pointers.

Alright, so you got the code to build cleanly.

Now Im sure you are curious how all this will work in a real embedded

board.

So, lets connect the TivaC LauchPad board and open the debugger.

The first thing Id like you to verify is the Rectangle constructor.

When you step inside, the first code you see is the call to the Shape

constructor of the superclass.

Lets step inside again and verify that the VPTR is initially not

set, but gets initialized to the Shape VTABLE containing addresses of

Shape_draw and Shape_area functions.

Also, please note that the VTABLE is definitely in ROM, because its address

starts with zeros.

Now, when you keep stepping, you return back to the Rectangle

constructor and there, the VPTR gets overridden to the Rectangles

VTABLE with addresses of Rectangle_draw and Rectangle_area functions.

So, your constructors in C work now exactly like the C++ constructors

from the last lesson, except that in C you had to add the VTABLE and

the code to set the VPTR manually, while C++ synthesized that code

automatically.

Now, the most interesting part is to see the virtual calls inside the

generic drawGraph() function.

But before you step there, just note that the first object to draw will be Shape

s1, the second will be the Rectangle r1, and the third will be another

Rectangle pointed to by the pointer ps3.

Once inside drawGraph, step to the Shape_draw_vcall() virtual call,

and take a look at the disassembly.

When you compare it to the code generated by the C++ compiler, you

can see that they are... identical.

So, lets step through this late-binding code a single instruction at

the time.

The first LDR fetches the VPTR from the me pointer in R6 and places

it in R0,

The second LDR fetches the first virtual function from the VTABLE now

in R0, and places it in R1.

The MOV instruction copies themepointer in R6 to R0 to prepare it

as the first parameter of the call.

And finally, the BLX R1 instruction executes the call to the address

in R1.

And indeed, you end up in the Shape_draw() method, because as you

remember the first pointer in the graph array was the s1 Shape class

object.

The second time through the loop in the drawGraph function the same

late-binding code now invokes the Rectangle_draw function, however.

So, as you just saw, the C implementation of virtual functions works

exactly like the C++ original, down to the machine instructions for

late binding.

At this point, Id like to note that the VPTR-VTABLE implementation

of polymorphism in C that you learned here is not the only

possibility.

In the literature or online you can find plenty of other

techniques.

Perhaps the most frequently used alternative is to remove the VPTR

level of indirection and embed the whole VTABLE inside every object.

The advantage here is a little simpler virutal call as well as a

nicer syntax more closely resembling C++, because the draw() method,

for example, is invoked on on object using the dot operator, like

this.

But the drawback is the increased RAM usage, because VTABLE is now in

RAM rather than ROM.

Not only this, the VTABLE is repeated in every object, so if you have many objects you can

easily double or triple your RAM usage.

An example book that presents this implementation method isDesign

Patterns for Embedded Systems in Cby Bruce Powel Douglass.

You can find there the whole chapter on object-oriented programming

and specifically its C implementation.

The author discusses Classes, Objects, Polymorphism and Virtual Functions,

Subclasses and other aspects.

There is also specific C code where you can clearly recognize the

VTABLE embedded directly in the attribute structure.

Interestingly, please note how this book also uses themepointer

naming convention for the class member functions in C.

The last subject I want to touch upon today is when to use

polymorphism and perhaps even more importantly when not to.

Lets start with a simplecode-smellindicating that polymorphism

might be helpful.

For that imagine how the generic drawGraph() function would be

implemented in a very traditional C-code, without late-binding.

Well, it would probably look something like that:

Specifically, you would probably add an attributekindinto the

Shape structure, and you would also provide an enumeration of all

possible kinds of shapes,

such as RECTANGLE, CIRCLE, TRIANGLE, etc.

Then every time you would need behavior specific to the kind of

shape, you would use a switch or if-then-else branching to invoke the

right function for the kind of shape you happen to be dealing with.

But such code is far less maintainable, because every time you add or

remove a new kind of Shape, you would need to find and change all

places throughout the code.

In contrast, the virtual call mechanism is not only much more

efficient.

It is also automatically extensible, because you can keep

adding and removing different shape subclasses, but you dont need to

change the code with the virtual call at all.

You dont even need to recompile the whole Shape base class, including

drawGraph() and any other functions like it.

So, here is your main guideline.

Whenever you see or anticipate code like the switch statement scattered throughout

your project, you should consider polymorphism.

But please remember that the only valid reason for applying

polymorphism is that the object-specific behavior needs to be

selected at runtime.

Which brings me to the guidelines when NOT to use polymorphism.

Well, you dont need polymorphism when the selection based on object

type does not need to happen at runtime.

The most frequent misuse of polymorphism I see in the industry is in

attempts to manage product lines of related, but slightly different

products.

For example, a medical company making, say, infusion pumps for drugs

might start with a single pump, but then keeps comming up with ever

more versions for various drugs and market segments.

The software for the new pumps is almost never created from scratch,

but rather is continuously tweaked and adapted from the existing

software.

But actually, the software for a new pump is not copied and

changed separately.

Instead, a single, ever growing code base is

extended to service all existing pumps.

And herein lies madness.

Developers litter the code with conditional logic, which quickly becomes unmanageable

and untestable.

This code makes decisions at runtime based on the product

type, version numbers and similar variables, while really any given

code set ends up in only one specific product.

Therefore, the selection of the product does NOT need to happen at runtime.

It can happen at compile-time and link-time.

So, even if polymorphism could eliminate much of the convoluted IF-

THEN-ELSE logic,

a better way is to design a clean Board Support Package (BSP)

interface and then provide different implementations, different BSPs,

for different products.

So, for product ABC, you would have bsp- underscore-ABC, and so on.

This is the use of abstraction, as I explained back in lesson 29.

Of course, there is much more to it than just a simple BSP

abstraction.

The effective management of product lines requires

careful *physical design*, which is the way you partition your code

into directories and files, such as header files and implementation

files.

That way, you can build the final software for any given

product by combining various modules at *link-time*, rather than

using techniques like polymorphism at runtime.

The art of good physical design is very valuable especially in

embedded systems programming.

Unfortunately, the subject is not widely known or appreciated.

While tons of books talk about logical design techniques, such as OOP, very few resources

exist for physical design.

One notable exception is the bookLarge Scale C++ Software Design

by John Lakos.

This book explains *physical design* really well.

Highly recommended.

And this concludes this group of lessons about object-oriented

programming in embedded systems.

In the next lesson, I will go back to my overarching theme, which is

introducing the main trends that shaped the modern embedded systems

programming.

Specifically, I will start a new segment, in which you will learn

about the last big trend that went mainstream during the 1980s, and

that is event-driven programming.

If you like this channel, please give this video a like and subscribe

to stay tuned.

You can also visit state-machine.com/quickstart for

the class notes and project file downloads.

The Description of Embedded Programming Lesson 32: OOP-part4: Polymorphism in C