As I’ve been developing my position on how software should be developed at the company I work for, I’ve come to a certain conclusion that I’m not entirely sure of.
It seems to me that if you are programming in C++, you should not use C style anything if it can be helped and you don’t absolutely need the performance improvement. This way people are kept from doing things like pointer arithmetic or creating resources with new without any RAII, etc. If this idea was enforced, seeing a char* would possibly be a thing of the past.
I’m wondering if this is a conclusion others have made? Or am I being too puritanical about this?
4
It is basically the way standard C++ is intended and encouraged (by the committee and the community) to be used:
- use C++ language idioms (mostly based on RAII, like smart pointers)
- don’t use C language idioms until you can’t avoid it (which still happen regularly, when interfacing with C interfaces)
This is what we have been calling “modern C++” for almost 10 years now. But most C++ developers start only now to realize it makes code looks like there is not much need for raw pointers, writing new/delete, and other error-prone constructs.
Now these constructs (C or not) are still there both for retrocompatibilty and for allowing you to write libraries to, again, free the other developers from having to play with them.
Also, C should be used to interface with C libraries, for low level constructs that require C-style code. Any other case, you can avoid using C idioms when you have C++ available.
For clarification: using C style doesn’t improve performance (assuming you understand C++ constructs and RAII). In fact, a lot of algorithms written in C++ are faster than the same in C, just because C++ give more info to the compiler to make him optimize in calling context (I’m thinking about template algorithms/types for example).
So performance is not necessarily a valid reason to use C idioms when you write C++.
14
C++ is a language that provides a lot of features that you may chose to use or not. Your decisions should be based on
a) Frameworks you use
b) The domain of your application
c) Your current code base
Frameworks
Some frameworks lend themselves to specific styles. For example, if you are using Qt/KDE/Wt you will usually use their memory model and will not have your own smart pointers. You’re unlikely to use exceptions as much. Qt’s MOC also has certain implications for templates that limits how much you can do with them.
At the same time, code that is heavily based on boost tends to be heavy in what others have described as ‘Modern C++’.
Then you have Google-stylized C++ which is essentially C with classes. Google C++ Style Guide
Application Domain
If you are coding software where performance is critical (financial, embedded,etc), a lot of the ‘niceties’ of C++ might go out the window in favour of limiting memory footprints or CPU cycles. There are situations where C is the ‘right’ language to use for a small part of the program, but not for the whole thing; C++ allows you to do that so that you can have your pointer arithmetic when doing crazy matrix math but have high level abstractions when combining the modules that use it.
Current Code Base
In the real world, if you are in a shop with 10 C coders who don’t want or like C++ you will have a much better time sticking to something like the Google style guide than something that is more ‘Modern’. If all your libraries are written in C and you are only gluing them together in C++ then char * might make more sense than going to and from std::string all the time.
Conclusion
C++ is not a langauge that is ideal in any academic sense, but it is a pragmatic compromise that worked out very well. So when using it, you should also have a very pragmatic approach to the problem and not get too bogged down on ‘The Right Way Of Doing Things’.
To answer your question: you may or may not be too puritanical, but that depends on the specifics.
3
If this idea was enforced, seeing a char* would possibly be a thing of
the past.
I tend to blend C and C++ together, making me that kind of funny “C/C++” developer. I think at least some of the reasons I list below will be legit hopefully. Often I’m using C interfaces with C++ implementations, or C++ interfaces with C-like implementations, though most of the time still C++ interfaces with C++ implementations.
There’s several reasons I reach for C interfaces or C implementations (in descending order of importance):
- API/ABI (interface)
- Exception-Safety (implementation: huh?)
- Lack of Type-Safety (implementation: huh?)
- Performance (implementation: rare)
- Build Times (implementation)
- Aesthetics (implementation)
API/ABI
A lot of the codebases I’ve worked on have always been rather large and designed for third parties to extend, with plugin developers actually basing their livelihood on writing plugins for our products. As a result, the central, abstract interfaces are actually software development kits for both internal and third party developers to use when writing plugins to extend the product.
The third party plugin writers use a wide range of compilers and standard library implementations.
With this requirement, using C++ interfaces at the core API level is often out due to the ABI difficulties associated.
If we started passing std::vector
through our central APIs, we end up facing ABI incompatibilities from one vendor implementation to another, and so we actually rolled our own vector-like container, but it’s fully standard-compliant down to fill ctors, range ctors, range erase, etc. with performance tests to make sure it matches up with std::vector
.
If we use vtables and virtual functions to model our abstract interfaces, we end up requiring plugin writers to use the same compilers we use, and will also break ABI the moment we introduce a single virtual function. As a result, the interfaces I design at the central core API level are C interfaces, mostly tables of function pointers, but with a C++ wrapper SDK which is statically-linked to plugins which wrap these into classes that conform to RAII and so forth. The wrappers are free to also use standard containers since they’re internally linked to the plugin.
If we used exception-handling across module boundaries, we also face the same dilemma, so I use C error codes at the module boundary level (but often translated to/from exceptions). The statically-linked C++ wrapper SDK takes these error codes and translates them back into exceptions, but safely throwing from inside the plugin to the plugin itself.
Exception-Safety Part 1
This is a weird one, but since throwing an exception during stack unwind leads to std::terminate
behavior, killing the process, it’s extremely dangerous if any kind of release of resources can encounter an external error. Another is recursively throwing from a catch handler when doing so would end up aborting an outer transaction which shouldn’t be aborted.
In these cases, occasionally we’re tempted to allocate a resource for some reason that could possibly fail (rare but sometimes necessary). For example, I might need to allocate memory in an obscure case during the release of a resource from a general destructor or the roll back of a side effect from within a scope guard’s destructor.
In those cases, sometimes a failure is not critical and can kind of be ignored (not ideal but perfect exception-safety is hard!). For example, failing to allocate memory might allow us to skip some work that’s a nicety, but isn’t absolutely crucial (like logging a message). In these cases, I find it easier to just use C-style error codes for failures to avoid the possibility of a recursive throw for functions which are often triggered in release/roll back/catch contexts.
Exception-Safety Part 2
When interacting with ABI, it’s not safe to throw exceptions to the caller from within a callee inside a different module, especially when caller uses a different compiler/build settings from callee.
As a result, we have to swallow up exceptions before they leak past API boundaries. Because of this, sometimes if I have a relatively simple implementation for a C API, I’ll just go ahead and use C all the way which can’t possibly throw so that I don’t have to worry about the possibility of throwing and sprinkle try/catch blocks at every entry point into the API.
Lack of Type-Safety
Okay, this is a weird one, but when we’re doing low-level communication with the hardware or treating memory as just raw bits and bytes, type safety can get in the way with code that inevitably has to have a whole bunch of really ugly explicit casts. An example is a fixed allocator provided through the central C API. In that case, I often just do the implementation in C instead of C++ since memory allocators don’t have much use for type safety — they’re working at the bits and bytes level.
Performance (Careful!)
This is very tied to the lack of type safety above. It isn’t necessarily that C style is faster or anything like that. It’s just that when it comes to doing things that are really critical with respect to how they’re paying attention with the interaction of the CPU cache and so forth, it’s hard to avoid things like raw pointers here and there.
Take a stride interface design like this:
void process_points(int num_points,
float* x, int stride,
float* y, int stride,
float* z, int stride);
It’s really hard to wrap that into a very safe design. The stride, after all, has to use a stride across these floats passed in that may not even provide a contiguous index access (they might just be skipping over bytes).
This unfortunately violates type safety, but these designs are often essential for those interfaces that handle a very heavy load with no better than linear-complexity requirements (have to touch everything), while still enabling potential optimizations like SoA SIMD access when each individual field has a stride that matches sizeof(float)
.
I’ve tried to make these types of interfaces semi-safe before by using all kinds of type-safe wrappers, but I didn’t like the inflation of build times and also just came to like the aesthetic (albeit lacking type safety) of plain old data passed through in bulk with custom strides (OpenGL API style).
This is only for very select areas of the codebase, like particle systems, and they also happen to be the designs I often write tests for the most, since they’re so dangerous.
Build Times
This might be slightly superstitious, but C just seems to build faster even if we’re including the same number of headers and so forth and still judiciously use pimpls and such in C++ when appropriate. It might just be due to my tendencies when I’m writing more C-like code, but as a result, I tend to use C when I’m implementing something that’s not very complex behind a C API to try to keep the build times down.
I really hate build times so I favor using Lua as much as possible for a large amount of code for anything that isn’t performance-critical (though LuaJIT even lets me do some things that are slightly performance-critical), then C++ for a middle amount of code, then C for a small amount of code for these reasons cited.
Aesthetics
This is probably the most biased section of them all, but occasionally I just like the lightweight, minimalist C aesthetic. I love C++ for its type-safety, ability to design rich interfaces and data types, genericity, etc. But there’s a C aesthetic I also admire which is just minimalist — dangerous but minimalist.
So sometimes I’m just in the “mood” for C. I’ll choose to kind of arbitrarily implement one of the C API interfaces with a C implementation for some of the simpler areas just because I’m in the mood for it. It’s a horrible reason, I know, but I wanted to include it because I think both languages are beautiful and can be blended together — one doesn’t have to supersede the other.
Conclusion
So anyway, these are the main reasons I use C or C-like coding from time to time even while primarily using C++. I don’t know if they’re great reasons or not. I try not to be that kind of dogmatic superstitious programmer, but I’ve found it beneficial to reach for C here and there, and essential for when it comes to ABI when targeting multiple compilers and possibly even multiple languages with a dylib API.
There is one case where you definitely should write in C style, and this is hardware-related programming. The main reason why the “raw C” is preserved in the C++ language is not just backwards compatibility, but the “raw C” also enables C++ to be used in bare bone hardware applications: when you are accessing hardware directly, when there is no OS present, or when you are writing the OS itself.
There is also the performance argument. Idealists will tell you that you shouldn’t use raw arrays, you should only use std::vector, and it should be equally fast as a plain array – if not then the compiler is bad. This is utopia, we have yet to see it. So far, STL and templates remain slower than “bare bone” code. However, the more C++ programmers that use templates, STL abstraction etc, the more pressure there will be on the compilers to produce faster code for it.
If the C features were removed from C++, then it wouldn’t be as much of a general-purpose language as it is today. It wouldn’t be used as much in embedded systems or performance-critical applications, it would become yet another desktop-only language like Java or C#.
0
Obviously the answer depends on what you do, so I will focus on one specific application I’m familiar with, high performance packet processing.
Network packets absolutely require you to access locations of memory by pointer arithmetic. You can hide this access behind a C++ class, but you cannot get completely rid of C style pointer arithmetic. The same is true of most binary file formats: you need to access raw memory blocks.
Also, many libraries are available only in C style. Actually, you will probably not find any good library that will abstract most of the C system calls in C++ style classes. You will thus need to implement the said library for the subset of the functionality you need.
So, the conclusion is that C style programming can be hidden behind C++ style classes, but in your professional life you cannot completely avoid C style programming. There is a reason for the backwards compatibility.
The new c++ has some advantages, this is much clearer than two ‘c’ style loops in i,j:
int array[5][10]; int i;
for_each(array, array+5,
for_loop(var(i)=0, var(i)<10, ++var(i),
_1[var(i)] += 1));
6
C will always give you a clearer view of what you are doing (you can find out all structure and understand it, for everything becomes intuitive from the moment you mastered the few reserved words and the few concepts like variable, function, pointer, vector, matrix (those within the concept of memory for further understanding) and the 3 Basic Structures (sequential, select and repeat).
In C ++, things get more and more obscure (they’re still intuitive, but it requires you to memorize more nuanced concepts to master it) every time you come across a concept that you’ll probably never be able to understand (different from C, which you will be able to understand your own structure after learning the language).
C as learning is irreplaceable (and you can continue to use it without any detriment …. and can incorporate C ++ with the greatest ease).
Can you use both with no problems.
1