While working on an embedded project, I tried to work in all C once and just could not bear it. It was so much that it was hard to read anything. In addition, I liked the optimized for built-in containers that I wrote, which should have become less secure and difficult to fix #define blocks.
The code in C ++ looked like this:
if(uart[0]->Send(pktQueue.Top(), sizeof(Packet))) pktQueue.Dequeue(1);
turns into:
if(UART_uchar_SendBlock(uart[0], Queue_Packet_Top(pktQueue), sizeof(Packet))) Queue_Packet_Dequeue(pktQueue, 1);
which many are likely to say is fine, but becomes ridiculous if you need to do more than a couple of "methods" of calls in a line. Two C ++ strings turned into five from C (due to 80-char string length restrictions). Both will generate the same code, so it doesn't look like the target processor!
Once (back in 1995) I tried to write a lot of C for a multiprocessor data processing program. A type in which each processor has its own memory and program. The compiler supplied by the vendor was a C compiler (some kind of HighC derivative), their libraries were private, so I could not use GCC to build, and their APIs were designed taking into account that your programs will be initialized / processed first / stop diversity, so interprocessor communication was rudimentary at best.
I had about a month before I gave up, found a copy of cfront and hacked it into make files, so I can use C ++. Cfront did not even support templates, but C ++ code was much clearer.
General, robust data structures (using templates).
The closest thing to C templates is to declare a header file with lots of code that looks like this:
TYPE * Queue_##TYPE##_Top(Queue_##TYPE##* const this) { }
then pull it in with something like:
#define TYPE Packet #include "Queue.h" #undef TYPE
Note that this will not work for composite types (for example, without unsigned char queues) unless you make a typedef .
Oh, and remember that if this code is actually not used anywhere, then you don’t even know if it is syntactically correct.
EDIT: One more thing: you need to manually manage code generation. If your "boilerplate" code does not have all the built-in functions, then you will need to install some control to make sure that all objects are created only once, so your linker has not spat out a bunch of "several instances of Foo errors."
To do this, you will need to place the non-embedded material in the "implementation" section of the header file:
#ifdef implementation_##TYPE #endif
And then, in one place in all of your code for each template variant, you should:
#define TYPE Packet #define implementation_Packet #include "Queue.h" #undef TYPE
In addition, this implementation section should be outside the standard #ifndef / #define / #endif litany, because you can include the template header file in another header file, but then you need to create an instance in the .c file.
Yes, it gets ugly. This is why most C programmers don't even try.
RAII.
Especially in functions with multiple return points, for example. remember to release the mutex at every return point.
Ok, forget your beautiful code and get used to all your return points (except the end of the function) goto s:
TYPE * Queue_##TYPE##_Top(Queue_##TYPE##* const this) { TYPE * result; Mutex_Lock(this->lock); if(this->head == this->tail) { result = 0; goto Queue_##TYPE##_Top_exit:; } Queue_##TYPE##_Top_exit: Mutex_Lock(this->lock); return result; }
Destructors in general.
those. you write d'tor once for MyClass, then if the MyClass instance is a member of MyOtherClass, MyOtherClass should not explicitly deinitialize the MyClass instance - its call is called automatically.
The design of the object must be explicitly handled the same way.
namespaces.
This is actually a simple task: just attach a prefix to each character. This is the main reason for inflating the source I talked about earlier (since classes are implicit namespaces). People C lived this, well, forever, and probably won’t see what a big deal is.
Ymmv