5

A employer is looking for C programmers, and I'm told they say that ...

Good C design isn't the same as good C++ design

... and so they're looking for candidates experienced with C and not only C++.

How is the design of a large C system (hundreds of thousands or millions of lines of code) very different from that of C++?

Are the skills required of a developer very different, what differences should an experienced developer expect?

I've read Why are most Linux programs written in C? -- including Linus' little "two minute hate" at http://harmful.cat-v.org/software/c++/linus -- but that doesn't answer my question, which might be, "How is a well-designed C system unlike well-designed C++?" Or are they similar, and is Linus' argument all there is to it?

I read Lakos' Large-scale C++ Software Design -- is there anything at all like that for C?


I'm trying to write this such that it isn't a duplicate of:

Please assume I already know the differences between the langages.

I used C in the early 90s (before C++ became popular on PCs), and for writing device drivers on Windows (in the kernel where the C++ run-time library wasn't supported), and I learned C++ incrementally as a superset of C.

IMO there's obvious mapping between C and C++, like what's written in one can be written in the other, for example:

  • C -- a "file pointer" or "file handle", plus an API of related functions which take a handle-or-pointer as a parameter, plus an underlying data structure (possibly hidden/encapsulated) which contains state associated with each handle
  • C++ -- ditto except that "data structure" and "associated functions" and encapsulated in a class, as data members and methods

C++ has additional syntactic and type-checking sugar (e.g. templates and operator overloading), and its destructors allow RAII and reference-counting smart pointers, but apart from that ...

And C has no first-class/language support for polymorphism, but e.g. a device driver on Windows is an installable plug-in, which has entry points which it exports, more or less like a vtable.

  • 1
    Pro: C++ does a whole lot of stuff behind the scenes. Con: C++ does a whole lot of stuff behind the scenes. – Caleth Jul 30 at 14:59
  • @Caleth I wasn't asking asking for pros and cons -- I'm asking what difference to expect in the design or architecture of systems implemented using them -- how is the design of a system implemented using C, different from that of one using C++? Apart from differences in syntax and which run-time libraries you're using, is the designer's model different? – ChrisW Jul 30 at 15:06
  • The designer's model is different in that they leave out all the things that C++ does implicitly behind the scenes. They design things to be done implicitly behind the scenes. There is far less repetition of the same idea. – Caleth Jul 30 at 15:10
  • Do you have any suggestions for improving this question? There are two votes to close as "Needs more focus", but I don't know what more (or what less) you'd want me to tell you. – ChrisW Jul 30 at 15:18
  • 1
    The problem with questions like this is that you're asking for context around a statement from people who did not say it. Maybe the person who said it had a specific situation in mind. Maybe it was said by someone who had no idea what they were talking about. How are we to know for sure? – Dan Wilson Jul 30 at 17:49
4

Look at this Linux kernel code, for an example of well-designed, idiomatic C code. Notice:

  • There is hardly any direct memory allocation or freeing happening in this file. People who are good at object-oriented design but not C design often have malloc and free all over the place because they are not accustomed to minimizing that in their design.
  • These functions are grouped by their semantic meaning. They are the file operations for ext4. People who are good at OO design but not C design tend to create more awkward, syntax-based groupings of functions, like everything that uses a certain type of pointer.
  • The primary data structures in this file are common to all filesystem drivers. People who are good at OO design but not C design tend to create different types for everything, like a struct ext4_inode * instead of just using the struct inode.
  • The data structures are passed into functions. People who are good at OO design but not C design often have difficulty arranging their code so the data flows through the functions like that, without passing everything everywhere. In OO design, classes primarily hold state, not function arguments.
  • There are two virtual function tables at the end of the file, but they are the minimum necessary to provide the abstraction of a filesystem. People who are good at OO design but not C design tend to overuse structures like this, or try to cram a bunch of data in there too, when something simpler will do.

I'm not saying you can't be good at both. Obviously, that's not true, but it's also definitely possible to be good at one and not the other. I could make a similar list for people good at C design but not OO design.

| improve this answer | |
6

The differences between C and C++ are so large these days that they are two different languages that require differences in how designs are expressed in those languages.

C offers one paradigm, procedural, for writing code where as C++ is multi-paradigm allowing a larger implementation vocabulary for implementing a design. You can use a procedural paradigm or a generative paradigm with templates or object oriented paradigm with classes or a functional paradigm with support from the Standard Template Library.

This difference in supported paradigms means that a C programmer often has to write C code in a procedural paradigm when C++ would offer a better and simpler alternative. The C programmer has to know how to translate from the abstract solution domain which may involve non-procedural concepts into the concrete solution domain within the constraints of what the C programming language offers.

A knowledgeable and skilled and experienced C programmer is quicker at this transformation and more inclined to use accepted practices and expressions. A knowledgeable and skilled and experienced C programmer is better able to read existing source code with understanding and to make changes that have less chance of introducing a defect.

Over the years I have learned from others or developed or found techniques that overcome some of the limitations that C has for large (as in greater than a million lines of source) bodies of source code. However doing so requires knowing C very well and having the experience with the language to work around its deficiencies and experience with other languages to know of those deficiencies in the first place. And often those workarounds provide opportunities for introducing defects by removing compile time checking such as using void * in argument lists.

The first thing to remember is that while the C++ standards committee has made great leaps of innovation in C++ between the original ANSI C++ to C++11 to C++17 to C++20, the C standards committee has made small changes.

The result is that the kind of well designed and organized standard libraries and capabilities available with C++17 require C programmers to cobble together a collection of third party libraries. And C++20 is coming.

The C programming language was not really designed for huge, multi-million lines of source code projects where as C++ is. C was used to write the UNIX operating system yet according to Wikipedia, Unix - Components,

The inclusion of these components did not make the system large – the original V7 UNIX distribution, consisting of copies of all of the compiled binaries plus all of the source code and documentation occupied less than 10 MB and arrived on a single nine-track magnetic tape. The printed documentation, typeset from the online sources, was contained in two volumes.

The namespace directive was added to C++ in order to meet the need of large bodies of source code to be manageable by partitioning out domains for names of classes, types, functions, etc. I've used struct with function pointers and a global variable as a kind of namespace approach for functions but you still can run into name space collisions with types and definitions. This is why the three letter subsystem acronym prefix naming convention is used with large C source code bodies.

C requires much more attention to detail than does modern C++. You can write modern C++ without using pointers and when you do use pointers, you have facilities that make pointers safer than what C offers. The result is that writing large bodies of source code in modern C++ can be much safer than writing in C and the error checking at compile time is better with C++ because the type system is more specific and less loose.

C++ offers more modern error handling than C with exceptions. Using exceptions can make error recovery easier and the use of object destructors allows for a more elegant and simpler cleanup in the face of errors. And you aren't required to use exceptions in those places where they don't make sense.

Encapsulation is easier and more complete with C++ classes and namespaces which leads to source code with better cohesion and less chance for defects.

Class constructors and destructors of C++ provide a capability for a defined starting and ending state that C doesn't have. And the ability to redefine operators allows for the development of true types that provide a straightforward and intuitive expressiveness of C++ source code that C lacks, a lack which requires work arounds and makes more cognitive demands on the C programmer.

Templates in C++ provide an immense power lacking in C and the C Preprocessor is in no way comparable to the capabilities of templates. The C Preprocessor is a separate component, a text processor that parses a file looking for text that looks to be a Preprocessor directive generating text which may or may not be C source code. This means that the kind of checking the C++ compiler does with templates is not available with the C Preprocessor and it also means that information available for writing templates is not available to define and Preprocessor macros.

A C programmer will have spent much more time with the Preprocessor and its idiosyncrasies than a modern C++ programmer who will rely on the more powerful templates instead.

The C++ Standard Library and Standard Template Library makes the C Standard Library look like a barely functional, crippled library.

Text string processing in C++ is so much easier and safer than C.

C++17 multi-threading support is much better than what C11 offers.

| improve this answer | |
1

"As ever it was since 'C++' was a 'C' compiler preprocessor," the fundamental idea is simply "to avoid pointlessness." 🤠

The "C" programming language was – just as in the late 1970's it was intended to be – "an enormous(!) step forward from [machine-specific ...] assembly language." (And it still proves its worth: the /arch directory of Linux remains comparatively focused and small.) But it was never really intended to be much more than that.

Therefore: today, unabashedly use C++. "Actum Ne Agas: Do Not Do A Thing Already Done!" The final object-code will run every bit as fast, but you will find yourself standing on the shoulders of giants – happily leveraging trustworthy code that you didn't have to write, or debug!

(It should of course go without saying that "sometimes you can't technically do that.")

| improve this answer | |
  • The final object-code will run every bit as fast I doubt that's literally true (e.g. the fact that any subroutine call might throw an exception adds some book-keeping) but it's fast enough for most user applications. There's a proverb that, "You needn't outrun the bear, you need only outrun the other person who the bear is chasing": similarly I'd guess that C++ will typically not outrun C but it doesn't have to, instead it need only be faster than the storage, the network, the display device, or the user, or whatever else it's racing. Even so this didn't answer the question, eh -- about C! – ChrisW Jul 30 at 22:34
  • @ChrisW think again. Typical exception handling has no overhead in the non-exception case, and you have things like std::sort, which beats qsort hands down with the same comparison on the same data, because it can be more aggressively inlined. – Caleth Jul 30 at 22:46
1

This is a challenging question, because the design may be influenced by the language but not necessarily:

  • there are a lot of examples of C++ code that is written as if it was C with some syntactic sugar for type management. Everyday on SO you find students posting C++ code from their teacher that is just C with classes and not at all in the spirit of modern C++
  • conversely, I’ve seen some C code that intelligently used struct with function pointers to achieve some level of polymorphism, and used setjmp()/longjmp() to achieve some degree of exception handling

Now, the core difference between the language is that C++ is object oriented, and the ensures a consistent object lifecycle behind the scene. This is the building bloc to a lot of other C++ features. C has no object lifecycle; you have to take care of everything by yourself: you’ve opened a file, better not forget to close it. You’ve allocated memory, make sure it’s initialized, etc. . As a consequence it is much more difficult to write reliable C code. When you design a new type, you have no guarantee that the struct members will be properly initialized, no guarantee of a destructor being invoked to clean the mess, no guarantee that a copy will not ruin it all by keeping references to dangling pointers.

So instead of thinking in objects with operations, you’ll mostly think in functions. These may use a struct* as argument (typical example: FILE*), or even keep their own resources. You also have to be prudent, since the struct’s state might not be as expected. This might lead to more defensive programming.

And since there is no exception handling, you need to keep care for anything bad hapening, inform the caller, foresee an error escalation until you’re back in a function that can cope with it. This part is the most painful and often underestimated. Note that this might require more goto as the linux kernel has demonstrated for C, but which is a useless statement in C++.

In the end, when designing with C language in mind, you’ll tend to focus on functional decomposition, manage encapsulation through compilation unit visibility, and most of all, keep things as simple as possible because that’s the best way to avoid errors. Lean is beautiful.

Important remark: I love both languages C and C++, and the question is not which one is better: it allways depend on the context and the problem you’re solving. But it’s important to acknowledge that it’s really different languages with different idioms, and different libraries, which requires different design strategies

| improve this answer | |
  • C++ is multi-paradigm and not only object oriented. You can write perfectly fine and lovely programs using C++ without using object oriented techniques. github.com/PacktPublishing/… – Richard Chambers Jul 30 at 23:58
  • @RichardChambers interesting link, thanks. indeed: you even have template metaprogramming which you don’ t have in C. – Christophe Jul 31 at 1:27
0

Just a guess but in the context of an embedded system a difference might be in memory allocation/deallocation.

Language support for polymorphism is nice but can easily be simulated when you need it -- e.g. a pluggable device driver can expose an array of function pointers, which behaves like a vtable implementing an abstract interface.

Apart from polymorphism maybe the key feature of C++ is destructors. These might be costly somehow at run-time, the object code is littered with places where a local object might be destroyed, and programmers are encouraged to use 'containers', maybe smart pointers, which encapsulate that even further.

Conversely perhaps on an embedded system, actions like copying data from one buffer to another is avoided (for performance reasons, it's better to pass around a pointer to the buffer); and allocating and deallocating buffers might be better avoided too (because any CPU for heap management is more than you might want, because you may get heap fragmentation in what's supposed to be an eternally-running system, and because you want the system to degrade gracefully (i.e. fail in predictable ways) if it's loaded or overloaded.

| improve this answer | |
  • "a difference might be in memory allocation/deallocation." That is one out of many. But usually easy to solve with modern c++ in a well as efficient manner as done in c. Also dynamically pluggable interfaces are a "piece of cake". – πάντα ῥεῖ Jul 30 at 17:55
  • C++ being a superset it's difficult to imagine what you can or would do in C that you "cannot" do in C++. – ChrisW Jul 30 at 17:59
  • 1
    Of course, I agree with that. But depends on the POV and what they currently have, and want to reuse as is. As mentioned: Hard to tell, without knowing their requirements and business cases in more depth. Well, at least BSD OS sounds promising, but again depends ;-). – πάντα ῥεῖ Jul 30 at 18:03
  • One argument to convince our "chief engineer of c in depth, and vanilla kernel maintainer" was introducing them to the CRTP and static (almost overheadless) C++ interfaces. Templates generally then a lot. I believe I fixed them on with the elegance of some stuff done with a bit of MTP, and a bit of non cryptic bit-fiddling. – πάντα ῥεῖ Jul 30 at 18:11
  • Yes and when I migrated a Windows code-base to C++ in the mid-90s that too started as "a better C". I think these people (i.e. the employer in question) make an IP filter driver, on FreeBSD (which I don't know), presumably on dedicated network hardware. They're an established vendor, more than a few employees, and started building it 20-odd years ago. I don't expect to change their architecture, was only hoping to foresee (get some insight into) what they might be talking about. – ChrisW Jul 30 at 20:00
0

C's toolkit is much smaller than C++'s. With C, you don't get

  • polymorphism
  • run time type determination
  • structured exception handling
  • built-in data structures (containers)

and a host of other useful features. Well-designed C code won't look or behave anything like well-designed C++ code because it just doesn't have the features C++ does.

For example, if your C code needs an associative data structure, you can't just instantiate a map with the appropriate key and data types, you have to write the whole thing yourself (or find a third-party library). If you want your container to handle a variety of key and data types, then you have to either use a bunch of preprocessor macros, or you need to create a generic back end that stores everything as void * and uses a bunch of type-aware callbacks to handle assignment, comparison, etc.

Memory management in C is labor-intensive compared to C++. malloc is not type-aware, it just reserves some number of bytes. You have to manually keep track of what's been allocated, and you have to make sure it's properly deallocated when no longer in use.

You have to make sure overflows don't happen, and you have to communicate any errors through return codes. You can fake exception handling with setjmp/longjmp, but it's ugly and non-intuitive.

I usually compare C++ to C programming as the difference between building a house with pre-assembled frame components and a wide variety of power tools (C++) to a pile of dimensional lumber, a hand saw, and a claw hammer (C).

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.