101 comments

[ 2.6 ms ] story [ 184 ms ] thread
As someone who writes a lot of C-like-C++ (system software, network daemons), I absolutely disagree. Using a template or a destructor here and there allows you write C++ code, which is as fast pure C, safer, and easily understandable to a C programmer.
Use from C++ just function templates, constants and stricter conversion rules - and you've got a perfect C.
Sadly that way it's just too easy to start using more modern solutions bit by bit and suddenly you are in object-oriented hell or worse.

Sometimes I've found that it's just easier to stick with plain C that way you don't ever feel tempted.

A bunch of modern C programs on Linux are now using __attribute__((cleanup)) which gives you nice cleanups/destructors. You can't compile a modern Linux distro without GCC/Clang support for this attribute.
And unportable code stuck to GCC C.

How ironic that commercial vendors get crucified for C and C++ language extensions.

It is not even close in terms of functionality to some C++ constructions that emulate Java/C# "finally". Although they are considered "non-canonical" by the C++ purists, I use them often, with great success.
I doubt it's really safer at that point.

You could use a few vectors or other container objects, which is a tradeoff - it gets a little bit more comfortable to spot/debug OOB issues (but valgrind and sorts can also help with the occasional problem). In exchange you are required to structure your data and code in a particular way to use these structures, which is a drawback. Another drawback is slower compile times (significantly so with std:: containers -- don't have a lot of experience with hand rolling C++ abstractions).

I often prefer having globally allocated (dynamic) arrays, which are unproblematic with respect to OOB or memory/allocation safety. I use a simple macro to wrap allocation of arrays to remove the boilerplate. It becomes like "realloc_array(pptr, FooType, 42)" or even "realloc_array(pptr, 42)". Local allocations are more often not easily dealt with using Memory Arenas, which is superior to alloc/free in constructors/destructor.

I even prefer using plain pointers with separate size variables for dynamic arrays. That's because "size :: array" isn't a 1::1 relationship. More than 50% of the time, I have more than one array that applies to the same size variable. Sometimes, there are multiple index/size pairs that apply to a single (or multiple) arrays.

I prefer staying with C because it keeps me focused. Sure, sometimes I wish for the occasional fancy feature, but then taking the time to think about a clean approach that solves the problem with limited features often benefits code quality and readability.

> I doubt it's really safer at that point.

the number of bugs that the compiler has saved me by e.g. having a simple Id<T> type tag instead of ints as used by every C library under the sun is absurd.

I don't know, I've tried approaches like that, even played with writing a few C++ helper functions, and even started a compiler with the goal of exploring ideas like this.

How do you go about Id<T>? The way I see that something like this can work out and give small advantages is that T would be an abstract type (a phantom or tag type, or whatever C++ guys are calling it). And all arrays that are meant to be indexed using Id<T> (while the array element types could be whatever) would depend on this type and have to be implemented using a special wrapped array type with overloaded operator[].

I don't know, this might come with many disadvantages.

All the stuff goes out the roof when you're now dealing with start indices + slices, i.e. doing "pointer" arithmetics.

My general feeling is that this Id<T> approach might show problems similar to the idea of using typed units, like wanting to prevent the expression "1W + 3s" but then making it terribly cumbersome to do real work, e.g. "1W * 3s" which would be perfectly valid.

In one C project I resorted to using about 20-30 typedefs like:

    typedef int Expr;
    typedef int Symbol;
etc. And I used these typedef names in function signatures. Variables also were named like expr, parentExpr, symbol, etc. It worked wonderfully. I had maybe 1 mistake where I submitted the wrong "type" in 6 months. And I'm someone who makes a lot of stupid mistakes.
> How do you go about Id<T>? The way I see that something like this can work out and give small advantages is that T would be an abstract type (a phantom or tag type, or whatever C++ guys are calling it).

in a very simplified way:

    template<typename T>
    struct Id { int value; };

    template<typename T>
    struct Identifiable { Id<T> id; };

    struct MyEntity : Identifiable<MyEntity> { string name; void foobar(); };

> And all arrays that are meant to be indexed using Id<T> (while the array element types could be whatever) would depend on this type and have to be implemented using a special wrapped array type with overloaded operator[].

    std::unordered_map<Id<T>, whatever> 
works fine (or your container of choice... flat_map, boost::multi_index...).

> It worked wonderfully. I had maybe 1 mistake where I submitted the wrong "type" in 6 months.

well, you're good, I remember when I introduced this in my code base, that caught a dozen bugs almost immediately - and I get prevented by the compiler to introduce more every couple months.

RAII has saved my bacon many times.
RAII has given my programs really fat chubby bacons many times. And made them harder to understand.
> And made them harder to understand.

Sorry to be blunt, but I'm sure that C++ isn't the problem here.

No problem, I see you had to be blunt. And thanks for your insightful comment!
A lot of times for a quick program I end up writing essentially C + C++ standard library, because a lot of C++ features related to better organizing of code are overkill when you have only a few files, but avoiding manual string management makes it worth using g++ instead of gcc
I think you can get far with this type of code organization, and using methods just for easier name lookup of functions associated with a particular type (static lookup -- no vtables / RTTI). Function overload sets are also useful. Libraries like https://github.com/skypjack/entt really help here too--for an 'ontology of alive objects' its felt like using an entity-component-system has been more ergonomic vs. a big tree of inheritancey classes.
I've always liked plain C better than C++ yet I sure wish they would've had a true string type like other languages. Most bugs originate from the str family of routines.
It's a widely spreaded misconception, that C++ is a superset of C. Nope. Modern C has ton of features, which are impossible in C++, from loosening requirements to implicit type casting (pointers to int) to main() recursion. They are different languages with different niches. I program both in C and C++, and honestly, I don't see modern application of C outside embedded, where C++ compilers implementation is impractical. C++ currently widely used in OS and drivers development, except Linux kernel of course, the only reason of that design approach is personal opinion of Linus Torvalds.
> I don't see modern application of C outside embedded,

C is one of the most popular languages for high-performance code. Most famous example would be the Linux kernel but literally anything that needs the best optimizations possible eventually comes down to C.

Why do you say that? Certain things in a language may increase binary size or something else but at the end of the day these languages all go through most likely the same compiler and should produce a roughly equivalent binary.
> these languages all go through most likely the same compiler and should produce a roughly equivalent binary

That's not really true. A single compiler may support many languages but that does not mean code written in those languages ends up as the same binary.

They will produce equivalent binaries, but not identical binaries.

For example, GCC's C++ front end will produce a different parse tree for the same C code as the C front end, resulting in a slightly different collection of basic blocks and data flow analysis (obviously, for very trivial examples, it will be identical). The result after all the gimplification and and different middle-end passes and rtl transformations you can end up with a surprisingly different set of generated instructions for the same code fed to gcc and g++. Equivalent, but different.

Nitpick: gcc and g++ are not different frontends but different compiler drivers. They can both invoke the C and C++ frontends (cc1 and cc1plus) and select them based on the file extension unless specified using the -x option. The difference between gcc and g++ is mainly in which libraries are linked by default.
Well, it's true that the different compilers are cc1 and cc1plus, but given most programmers I've run into here in vacuumland have trouble distinguishing between their IDE and their toolchain, expecting them to understand the difference between the actual compiler invoked by the driver depending on a file extension or '-x' switch vs. the different compiler driver that invokes that compiler by default for a given file extension is a bit of a stretch. Even you seem to be confused by the compiler proper (cc1 or cc1plus) and the front end (the part of each compiler that parses the code and builds an internal representation that then gets passed on to the middle end for optimization and the back end for register allocation and instruction generation).

The fact is, the front ends of cc1 and cc1plus will interpret the exact same C code differently to the extent that code containing undefined behaviour will have remarkably different results in the generated instructions. The differences between the cc1 and the cc1plus compilers are not limited to what the compiler driver passes to the linker, they have completely different front ends and only share the middle and back ends.

> Even you seem to be confused by the compiler proper (cc1 or cc1plus) and the front end (the part of each compiler that parses the code and builds an internal representation that then gets passed on to the middle end for optimization and the back end for register allocation and instruction generation).

Sure, the cc1 and cc1plus binaries come with a middle and backend linked in, but what differentiates them is the front end. Not so much for gcc and g++ where both `gcc file.cpp` and `g++ file.cpp` will invoke cc1plus. Not to mention that with LTO cc1 and cc1plus do almost exclusively front end work while everything else happens in lto1 (for both C and C++).

I worked under ULL trading platform, they used C++ with heavy templates, without RTTI and exceptions. If things can be done in compile time, it should be done - which is the purpose of templates. Neither our nor neighbor teams (near 800 developers) used plain C. As Bjarne Stroustrup told, there's no place between C++ and machine code for "more low-level language", everything which could be done in C, also could be done in C++ with the same efficiency.

Good tendency however, now both C and C++ developers started experimenting with Rust, probably creating unified community and platform.

The problem is the one you state.

You have to use a subset of c++. Nobody agrees what that subset is. You have to be super-vigilant in code review. Features invite their use.

It's the lack of features in c that make it attractive. No magic. You want an object or a virtual, code it up if you really mean it is not just a keyword. You take responsibility for all the code running.

Every time I've cut a tonne of latency from a trading engine it's that. People relying on library and compiler without taking responsibility for it. STL is great. Except if you're actually performance critical, when you can beat it easily by solving your problem, not someone else's solution for everyone's possible similar problem.

With the caveat that I am NOT suggesting this justifies choosing C over C++, I just wanted to mention this talk about how "zero-cost abstraction" is an idealism, not necessarily a reality: https://www.youtube.com/watch?v=rHIkrotSwcc&t=17m30s

That said, I tried to reproduce something similar, and it seems the issue only occurs in my example due to external linkage (adding 'static' fixes it)... but I can't claim this will always resolve the issue: https://gcc.godbolt.org/z/1vbqo3

I watched only a tiny part of that talk but the speaker claims "there are no zero-cost abstractions" and justifies it with.... unique-ptr??? I mean, who doesn't know that smart pointers can be slower than raw pointers? They're not there because they're "zero-cost", but because the improvement in reliability & readability more than makes up for the runtime cost in the vast majority of situations.
He's comparing unique_ptr to just plain old-fashioned new and delete. Is it obvious to you what the actual cost is in moving between these? It's not really obvious and probably not what you think. I would listen to the full section of the talk on unique_ptr.
I developed an optimizing C++ compiler in my youth (one that was actually used in production, not just a pet project). I probably know the actual cost, and also know it may depend on compiler.

But that's not the point - the point is that smart pointers never claimed to be zero-cost abstractions, AFAIK.

> But that's not the point - the point is that smart pointers never claimed to be zero-cost abstractions, AFAIK.

> I mean, who doesn't know that smart pointers can be slower than raw pointers?

Nobody... on what basis? If you're not in that set of people, that doesn't imply the set is small or empty. Googling suggests lots of people have given such advice, and, may I suggest, it's not because they were stupid.

> Nobody... on what basis?

I don't understand the question (I didn't use the word "nobody"). I stand by the first claim (library writers & ISO C++ commitee never claimed that smart pointers are zero-cost abstractions; so why would people believe that? How did that claim start?).

The second (rhetoric) question... well, of course people would occasionally believe random stuff, for God's sake, we have (non-stupid!) people who believe COVID is a conspiracy. That said - there's no basis in the belief that smart pointers are zero-cost, so it's funny to make a talk "debunking" it - to me it looks similar to a talk that "COVID is real, guys!". I was genuinely surprised this talk is needed. I think if this is really a wide-spread belief, a more insightful talk would be about how it got to be a wide-spread belief :)

Chandler is on the cpp committee and personally said he was surprised by this cost when passing unique ptr as a parameter, as were many on the committee.

So the evidence against your claim is literally within the talk.

Ok, I guess I'll look at the talk/ didn't know the guy was in the C++ committee. Still, I personally had to fight the other way around - to convince people that "no, smart pointers don't have significant overhead over raw pointers; and the optimizations/ custom smart pointers that you're doing now, to the extent that they have any effect at all, will likely be rendered obsolete by future compiler versions & libraries".

I could understand a CPP committee member expecting a certain compiler to optimize a certain situation; however, the blanket statement that "smart pointer X is zero-cost" is more than strange, given the fact that it's always bound to be implementation-dependent, and there isn't one single C++ compiler (or even a "canonical" one) so that you could make that claim, at all. I find it really suspicious.

[edit] I watched the part of the talk - I think he was a bit surprised that has favorite compiler didn't perform that optimization, and it took him some investigation to see why. I don't think he truly expects smart pointers to be "zero-cost abstractions", I think he was just surprised that his compiler didn't optimize better that particular situation and had to dig in to find why. I still find the whole presentation a bit of a misleading stunt - there _are_ zero-cost abstractions (for some definitions of "cost" of course; and/or in some situations). E.g. in rust [1]. And even in C++ - a local unique_ptr is _probably_ a zero-cost abstraction!

BTW.. in his example - just use move semantics, put the pointer as the 5th argument, and you'll probably have the same runtime cost (ie both the raw pointer and smart pointer methods will compile to same code; since the ABI no longer allows you to pass the raw pointer via registers, it goes to the stack, so you have the additional load that bothered him in the raw pointer case, too).

[1] https://medium.com/ingeniouslysimple/rust-zero-cost-abstract...

> BTW.. in his example - just use move semantics, put the pointer as the 5th argument, and you'll probably have the same runtime cost

He talks about this. C++ doesn't have destructive moves like rust does, which is the root cause of why you cannot make the unique_ptr cost zero. It'd take an ABI change to fix this. This is precisely why the scenario is interesting. A lot of people (including you) thought that "just use move semantics" would solve it.

Thing is... talks like these give a wide body of application developers ammunition to say "smart pointers are slow, we need to use raw pointers!". Which is incredibly damaging. In many real-life situations, the difference literally doesn't matter (one more memory move? pfft... I'd take the whole complicated, exception-safe function body over knowing that in case of an exception I won't have a memory leak)

There are indeed cases where this sort of difference in performance matters; But those are a vanishingly small group of people; and as far as those people are concerned - they typically look at assembly anyway.

> smart pointers are slow, we need to use raw pointers!

He addresses this in the QA at the end. Chandler explicitly says "I still believe you should use unique_ptr and pass it by value". If somebody watches this video and takes away "don't use smart pointers" then they weren't paying attention.

I've been wondering why they don't permit destructor elision after a move into a newly constructed parameter. I have a hard time seeing how this would break reasonable code, and even then, we can probably find a backward compatible workaround (or rely on a compiler flag). Any idea?
ABI. C++ defines rigorous calling conventions that don't permit this. There are some on the committee who want to permit ABI breaks to enable these sorts of optimizations (in this case, destructive-move) but the committee in general has been super hesitant to permit ABI breaks.

It won't break reasonable code. The problem is it breaks interop with compiled binaries.

I'm confused, this seems unrelated to ABI. It's entirely up to the caller whether to subsequently destroy the moved-from object or to skip doing so. Whether or not the caller does so is irrelevant to the call or how the parameter gets passed, and it wouldn't affect the moved-to object (whose destructor always runs). The destruction of the moved-from object (if not elided) happens long after the call has returned - at the end of the object's scope.

I think you're confusing my proposal with the pass-in-register proposal? Or maybe I'm missing something.

The "zero-cost abstraction" concept is that you don't pay for something you don't use. If you use a smart pointer, you pay for it. If you don't use a smart pointer, it's zero cost.

Any argument that smart pointers are not zero cost to the language because they have a cost when you use them is a classic straw man argument.

I think there might be a terminology mixup here but he probably meant zero-overhead abstraction. And in any case, the point he's making is not a strawman argument.
OK, let's substitute "overhead" for "cost in the argument.

Premise one: C++ has zero overhead for most of its features, like smarts pointers: if you don't use them, they cost nothing.

Premise two: using features like smart pointers adds a cost to your C++ program.

Conclusion (due to the strawman logical fallacy): C++ is not zero overhead because there exists a feature that has a cost if you use it.

I'd like to see the reasoning that leads to the same conclusion without resorting to the strawman logical fallacy.

(comment deleted)
He talked about more than just unique_ptr if you actually watch the video, but I don't even get why you're dying to have such a pointless argument here. When you see free lunches offered somewhere, do you start arguing with people how everyone is wrong and there is such a thing as a free lunch too? Is it so hard to actually take the point and just move on instead of dissecting it like a mathematical theorem?
What's your judgment on the Doom 3 source code? It's something performance critical, but it's in C++; Sanglard's review of the design codebase was very positive.
John Carmack wrote in 2010: > There is some decent code in Doom Classic, but it is all C, and I would prefer to do new game development in (restrained) C++.

> I had been harboring some suspicions that our big codebases might benefit from the application of some more of the various �modern� C++ design patterns, despite seeing other large game codebases suffer under them. I have since recanted that suspicion.

I think this shows that C++ works better in controlled (i.e. corporate environments), whereas C is often preferred in Open Source.

Strange thing to say, to be honest. Many of the most performance-critical industries are heavy on C++, not C. Games, finance, high-frequency trading, where every microsecond is worth huge money -- these are typically C++ shops. The strictness and ability to move things to compile time are features not available in C.
Any examples of using compile time features that make a difference, instead of making code harder to maintian and increasing compile times significantly?
You’re talking about really two different issues here. I don’t think anyone would suggest templates can’t get complicated. But they are certainly the doorway into maximum performance over C.
No I don't think I'm talking about something else. And I wouldn't submit to the opinion that performance cricitical code needs to be written in C++.

For example, Games are heavy in C++ where there are a lot of abstractions. Engine code is mostly C style as far as I can tell. Optimized code is code that exploits specific properties of systems, hence it is significantly less abstracted than higher-level code.

The stereotypical std::sort example doesn't count, really. There are many reasons why std::sort vs qsort (which are both generic sorting implementations - i.e. not optimized ones) is not significant in practice, but it's still among the most frequently cited examples - hinting that there might be few real wins (with regards to performance) from all this compile time and templates stuff.

Hm, I’m not sure where to start with your assessment here. Your comments on sorting don’t really seem relevant, I don’t think you’re referring to the same kind of compile time programming we’ve been discussing. There’s a difference between generic code, and code that runs at compile time, even though both might actually use the C++ feature of templates. C++ templates are way to write code that supports generics, but they are also a way to do a style of programming referred to as “meta-programming“, these are really not the same thing at all.

While you can certainly do without generics in the language, as proven by the success of many many languages, perhaps at the inconvenience of some developers, compile time meta programming is an entirely different area that only few languages really support. The high frequency trading firms and the game studios I have partnered with are typically using C++ specifically for those compile time programming features. They make dramatic performance differences in a wide range of highly specialized algorithms.

It wasn't me who said "templates" first. (I don't think of them as a practical way to do compile time computation).

I was mostly looking at constexpr and whatever similar things have appeared in C++ lately. And I wanted to know about actual applications of them in the wild that make a difference.

Because, yeah I can precompute a 100K hash table or whatever at compile time, but I can also just do it at program startup (would anyone notice?) which is by far the simplest thing to do. Or I could just generate the data in a separate build step which is probably more hassle compared to constexpr but also probably friendlier in terms of build times in practice.

In embedded systems, often times not all memory is equal. Pre-computing a lookup table at runtime may not be practical due to the limited amount of RAM vs. flash memory. A constexpr or template meta program is, as you touched on, a nice way to do calculations at compile time in the existing language without having to add an explicit autocoding build step. An explicit build step eventually makes sense for sufficiently complex algorithms, but it can be a lot of build system maintenance overhead for small to medium complexity stuff. Implementing esoteric code using obscure syntax may be bad for readability, but keeping it "in the language" has a benefit of limiting the amount of project specific knowledge required to understand it.
Now this I call a reasonable comment! I learned something, thank you.
Thanks! Thinking a bit more about it, I could imagine some performance impacts even on full featured CPUs. With virtual memory and the OS paging stuff in and out of physical RAM under memory pressure, read only data can be swapped out faster than writable data. The former, being immutable, can just be forgotten and then reloaded from disk when it's accessed again, while the latter has to be written to a swap file first, and writes are typically orders of magnitude slower than reads. Doesn't matter as long as you have plenty of RAM though.
Another benefit on that line is that read-only memory can be shared between processes.

I'm not sure that this is hugely relevant these days for small stuff, though. Like < 1MB... how many instances of the same program do we have running simultaneously, anyway?.

Matrix compile time templates like Eigen result in vastly faster code than doing it in C, since many operations can be compile time simplified. C has no way do do this at compile time.

This is just the tip of the iceberg on using templates and classes to make faster, cleaner code.

Indeed Eigen is a popular library for its expression templates that make many math operations much faster.
In C, you could provide a bunch of functions that chain together the permutations of operations that can be optimized. I.e. TransverseMultiplyMatrixInverseDotProduct() or whatever actually makes sense. Since you can't overload operators, folk would have to read through the available functions to find what they need anyway. It wouldn't be pretty, but it would be functional and probably compile down to similar machine code.
Of course you could also just write assembly if you wanted the exact most optimized machine code.

It’s also hard to generalize these things in the form of those kinds of macros. Whereas with something like Eigen, just write your code like normal, you don’t have to worry about the special cases, and the compiler rewrites it for you. That’s one of the nice benefits, one of many, of metaprogramming.

No, you cannot, not without putting an incredible amount of work on the programmers plate.

Consider the simple problem of multiplying together a sequence of N matrices of possibly different sizes with the least amount of work. The order you multiply in is determined via some optimization technique. You can try to have a different C function for each N, but eventually you will have some N for which your lib doesn't have the call. Or maybe you'll try to pack pointers into an array and pass that, which is now slower and more memory costly. In any case the order must be solved at runtime.

Templates allow, at compile time for known size matrices, the order to be determined. This cannot be done in generality with C since you cannot in C do it.

And, if the matrices were constexpr, this can be computed at compile time.

So the template method, giving you Turing complete operations, can do things that you cannot do in C.

This is just a simple example, the tip of the iceberg.

I'm not disputing that you can make prettier, more scalable APIs in C++ than in C. My point is that it's not completely hopeless in C either, though. In practice, the user of a matrix math library needs to understand the operations they're doing, and especially so if they actually care about performance. In the example you gave of a string of matrix multiplications, matrix multiplication isn't commutative, so the order is the order that the programmer wrote them in. The compiler is still free to reorder and coalesce redundant calculations with sufficient inlining. Also, N is small for 99% of use cases where performance matters, and when N is large, falling back to a slower "runtime" implementation is perfectly reasonable because the runtime overhead is insignificant compared to the overall cost of the operation; eigen itself does that internally. A blanket claim that pointers are "slower" and memory costly also seems a bit overly simplified. They are usually worse than passing by value for small data sizes, but for larger data sizes, some sort of reference passing somewhere will be faster than doing unnecessary memory copies. For sufficiently large data sizes, a straight forward hand written "runtime" algorithm implementation may even happen to be faster than a compiler generated specialized equivalent depending on the hardware's memory model.

Eigen is a great library and very convenient to use. It's great to be able to write straight forward chains of matrix operations and trust that the resulting program will be reasonably fast. There's no need to be dogmatic about C vs. C++, though. They're both higher level languages targeting the same underlying hardware. Templates enable library developers to make simple APIs at the expense of more complicated library implementations. In C, it's often necessary to compromise on the simplicity of the API to achieve the same performance, but it also generally means that the library implementations are simpler. The overall quality of the resulting binary can be about the same, and is almost certainly within the same ballpark performance wise. As an embedded engineer, I often need APIs that are compatible with C whether or not the implementation is C++, and I value simple library implementations over complex ones; the libraries and my use cases are often obscure enough that they are buggy, and so the more readable the library is, the easier it is for me to debug them.

As a recent real world example, a coworker, who is a wizard that knows way more than I do about signal processing, implemented some matrix heavy algorithms in a high level language that supports just-in-time compilation down to parallelized CPU and even GPU machine code. It worked great on an x86-64 workstation, but on production hardware, we struggled to get the code to run fast enough; it would peg all the CPUs at 100%. The many layers of libraries and JIT compiliation made the system very hard to debug even after a couple weeks of trying. I suggested re-implementing the algorithm in C++ using whatever matrix library was most convenient, and a few days later the system was running perfectly and averaging 14% of one CPU. The algorithm went from maybe 50 lines of very readable code to 250 lines of relatively ugly code, but we understood what it was doing way better. I believe he used Eigen in the C++ implementation, but whether or not the matrix library was optimized at all, C, C++, or rust, it still would have sipped around 14% of one CPU. My point is that, when performance matters, you need to understand what the software and hardware is doing, and so there's value in simplicity and pragmatism.

> It wasn't me who said "templates" first.

But you were talking about generics, which implied you were incorrectly conflating that with template meta-programming, since generics are done with templates.

> I don't think of them as a practical way to do compile time computation

Templates are however the most flexible, advance technique for C++ compile-time programming, though the C++ standard is evolving to bring more and more of those features into the language without using templates, i.e. "constexpr if".

My recommendation would be to avoid speculating on what the benefits are of a language or its features if you clearly don't have serious experience using them. It's fine if C++ is not for you.

> But you were talking about generics, which implied you were incorrectly conflating that with template meta-programming, since generics are done with templates.

I'm aware this is a pointless discussion, but please double check your claims are right if we are in "check mode". I did not say "generics".

>> Any examples of using compile time features that make a difference, instead of making code harder to maintian and increasing compile times significantly?

> My recommendation would be to avoid speculating on what the benefits are of a language or its features if you clearly don't have serious experience using them. It's fine if C++ is not for you.

Maybe you shouldn't make such statements if you don't know about my experience. I am speaking from experience, and exchanging subjective experiences isn't worthwhile most of the time, but sometimes (if people don't go down to personal attacks) there is a new viewpoint to find.

> I did not say "generics".

FYI if you’re talking about compile-time programming and you use the word “generic“ (as you did, in the context of a generic sort, which actually does have a meaning related to generics, as the routine works on containers of any type), note that this is a well-established term, which could be confusing if you actually are referring to something else.

Yet the 'modern web browsers' we're all using are probably written in C++.

I'm yet to see a 'modern web browser' that is fully written and only written in C. (C bindings don't count)

Original cross-platform Netscape was iirc. Quality died when they went to c++? Probably less influential on their fate than Microsoft.
> Quality died when they went to c++?

when they went to c++ it became Firefox. do you think we'd have a better firefox today had it stayed in C ?

/When/ they went, which, iirc was Netscape 4 (?). It was horrendous, crashing, slow buggy, awfulness, which the previous version really wasn't. This was when it was all closed and proprietary.

Firefox came a long time later. Well after Mozilla opened.

No idea if Firefox would be better if they'd stayed in c. What i do know is that c++ is a vastly, vastly better language now through no fault of the standards committee. G++ got good. Then came clang++, valgrind was an amazing leap forward too.

Before all that there really was no such thing as cross platform c++.

(comment deleted)
pedantically C++ is not a superset of C, but they share a large subset that happens to be closer to the whole of C than the whole of C++.
Are any of the C features that aren't present in C++ actually useful for something that doesn't have a better C++ alternative? I can't see main recursion as being particularly important (and I suspect a lot of compilers will let you do it even if the standard doesn't).
C99 designated initialization is the big one. It almost feels like a new language and makes C++ look a bit silly with all its different initialization features. C++20 got a subset of C99 designated init, but it is so much restricted that it's practically useless for anything but very simple structs.
> but it is so much restricted that it's nearly useless.

that's a pretty harsh take on it - how would you design it considering that initialization order has to be respected (else a lot of use cases would break; it's definitely not uncommon to have a member of a struct to depend on a previous member being initialized) ?

I have yet to find a particularly blocking use of the feature when test-driving it - it's on the other hand a great boon for readability (and definitely puts a nail in the coffin of named function parameters proposals imho).

The ordering requirement is indeed the biggest problem once you have structs with more than a handful of items (maybe one day Intellisense will help with this problem), but the missing array access and not being able to "chain" designators (.a.b.c = ...) are also painful omissions.

Clang has demonstrated long before C++20 that it is possible to use the full C99 designated-init feature set in C++, so I really don't understand why the C++ Committee had to cut down the feature when there was already a fully working implementation.

PS: as an example for where C++20 designated initialization breaks down, see for instance [1], I tried to replicate this code in C++20, but quickly gave up (with the main problem being that it is impossible to remember the correct declaration order in big "option bag" structs like this.

[1] https://github.com/floooh/sokol-samples/blob/e0a93a21b17a663...

Yup, designated initializers is a big one why I prefer C to C++.

Another big one is implicit casting of void ptrs. I cast void pointers to specific types A LOT. There are many situations where you have to cast void pointers (that come from an abstract API that only deals with untyped memory) to specific types (higher-level application code), and I can't really be sure, but I think there isn't a nice way to do that in C++.

It might be because C++ wants you to use their elaborate abstractions and templates, which in many situations is just fluff that makes programs harder to read and maintain.

You can cast void pointers in C++ as well, and it is very easy. The difference is that usually you would use a specific kind of cast that makes this clear to the reader.
reinterpret_cast was the canonical way of doing it in C++, but now I believe that this is actually UB and so modern C++ should use std::bless.

C++ also supports C-style casts but it's a code smell.

Anonymous struts and unions allow fine grained control of memory layout in embedded code. (I am not 100% sure this is not UB but I have seen it in a lot of embedded code)

Not sure if this is impossible in C++, I think some of the alignment functionality added in the past several versions might cover this, but it is not as simple.

IMHO, Not a big reason since every compiler offers some non-standard keywords to cover this use case, but a reason nonetheless

There is a ton of useful libraries written in C [1] [2], and I feel like there's a little revival in the last few years for using C to write "bedrock libraries", because such libraries are often easier to integrate into "foreign language" projects than C++ libraries. Those libraries also often end up in C++ projects because they are often easier to integrate into C++ projects than actual C++ libraries.

[1] https://github.com/floooh/bookmarks

[2] https://github.com/nothings/single_file_libs (here, C/C++ usually means "written in the common subset of C and C++")

Linux, BSDs and all other UNIX-like systems, PostgreSQL, Redis, Nginx, HAProxy, interpreters of Python and many other languages, tons of libraries, ... and the list could go on and go on.

A lot of very successful and robust software is written in C. The preference for the C language is certainly not due to a personal opinion of only one person.

C does not have implicit casting from pointer to int: that is a constraint violation which requires a diagnostic from the compiler.
I had never even thought of main recursion before, so I tested it out. It worked fine in both C and C++; am I misunderstanding something?
It's weird to conflate C as a language and its ABI, as the glue between other languages. The glue in question is the ABI, period. Rust does not interface with C, it interfaces with a C library (dynamic or static) that is in a standardized format.

C++ attempts something messy, which is to take C source and generate objects conforming to this ABI. In doing so, the only sane thing to do is to respect the C spec to transform C source in a binary blob that others can understand.

C++ should stop attempting to parse C. Just use a C compiler, create a binary blob and interface with it from the C++ compiler. Attempts to make both language compatible on some common subset makes them both worse off.

IMHO C++ should continue to be able to parse C declarations (e.g. include a C header which only has declarations in it - in fact, all languages with C interoperability should have this feature), but otherwise I agree, C++ doesn't really need to be able to compile C code because (usually) it's trivial to setup a mixed project where C source files are compiled with a C compiler, and C++ source files are compiled with a C++ compiler.

This requires that the Microsoft C compiler is updated to the latest C standard though. Until recently their recommendation was to compile C code with the MSVC C++ compiler.

A C header can contain function definitions, though - either using the inline keyword (since C99) or as macros. Moving all functionality behind a function call barrier may not give satisfactory performance.
Yeah, adding the inline keyword to C wasn't such a great idea in hindsight because it muddled the separation of interface and implementation (LTO is the better solution IMHO). A possible workaround would be that such a "declaration parser" would ignore the function implementation block (but inline code would make the library unusable from C++ anyway). As for macros: they would simply be resolved by the declaration parser, and either the result is parsable as declarations or not (which would then be an error).
Doesn't LTO interfere with debugging massively?
> e.g. include a C header which only has declarations in it

That seems like an awful amount of work just to prevent C++ programmers from using any existing C library.

Clang has been able to parse MSVC headers and link with its libraries for years now. It also supports the same interface that CL provides using clang-cl, so it's not that that necessary for Microsoft to actually update the entire compiler; they just need a few library changes in order to fully implement C99/C11.
Microsoft dropping their own C compiler and using Clang as the standard C compiler in Visual Studio would be great TBH.
They probably haven’t yet because of backwards compatibility. I’m certain there’s plenty of companies that take advantage of the quirks unique to MSVC++ and would be quite upset if it was just replaced with GCC or LLVM.
This, exactly. MSVC is full of extensions so heinous and horrible to make you scream when you sleep at night. They probably pledged to support those until the end of time, so I guess they would have to keep CL around forever.

Also, if you manage to overcome how horrible their CLI tools are, Microsoft's CL is a nice C++ compiler. Their STL support is great nowadays and lots of things just work; I even had seen times where they were _stricter_ about standards than Clang/GCC, which honestly shocked me.

Good article! I very much agree about C and C++ being philosophically different. When ever someone asks me a C programmer if I want to switch to Rust, I think “but thats a replacement for C++ not C”.

I think the ship has sailed on proper compatibly, and thats ok with me, let them be different. More compatibility is not worth braking any existing code.

> When ever someone asks me a C programmer if I want to switch to Rust, I think “but thats a replacement for C++ not C”.

What do you make of Zig?

I think It has some interesting ideas but many of them dont need a new language. I also think that Zig like C++ thinks that the language is the problem that needs solving.

As a C programmer I dont want clever features. I want the simplest possible where nothing happens without me explicitly typing it. I'm much more interested in better debuggers, and tool chains [1]. Every time you create a new language or a significant new version of a language, you have to start over with the tool chain.

[1] https://youtu.be/pvkn9Xz-xks

A more appropriate question is: The problem with C++ compatibility in C++. The C++ I wrote 20 years ago doesn't look anything like C++ of the modern day. The C is the same.
I’ll admit I’m partial to C++, but couldn’t one argue that that’s because the C committee is just slow to add features to improve the language? I’ll admit, there’s a lot in the C++ standard library, but with zero cost abstractions, if you don’t use, say, `std::map<T,U>`, you don’t pay for it.