If this idea was enforced, seeing a char* would possibly be a thing of
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)
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
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.
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
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.
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.
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.
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.