9 ms·
C constructs that still don't work in C++
- jalospinoso 5mo agoI wrote this after repeatedly seeing experienced C programmers hit the same sharp edges while moving into modern C++ codebases. Many of these differences are intentional and defensible from the C++ side. But some are still surprising because they invalidate patterns that were historically common, performant, or idiomatic in C. The interesting part to me isn’t "C vs C++," but where the languages diverged philosophically: object lifetime vs raw storage, stronger type systems, implicit conversions, ABI and optimization assumptions, and the boundary between "portable" and "works on my compiler." I’d also be curious which C constructs people still genuinely miss in modern C++. For me, restrict is still near the top of the list.
- hgs3 4mo agoNot sure if you're aware, but defer is proposed for C2Y [1]. It's already available in Clang behind a compiler flag. It is interesting how the languages continue to diverge. [1] https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3734.pdf https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3734.pdf
- pjmlp 4mo agoBecause the communities aren't the same. C++ is 1990's Typescript for C++, while C folks still think is a portable Assembly instead of designed to an abstract machine model. As such C++ community embraces high level abstractions and type systems improvements, whereas C wants to still code as targeting classical hardware.
- jstimpfle 4mo agoCaring for the actual assembler output in selected critical pieces of code is not the same as ignoring the abstract machine model. What you claim is simply not the case if you check actual proficient systems programmers. Of which there are an astonishingly high share C and C++-but-mostly-C programmers.
- pjmlp 4mo agoAny user of compiled languages cares about Assembly, which is why regardless of the compiled language, an Assembler was always shipped alongside. Also it isn't a C invention to have the compiler dump the Assembly output instead of object code. Now the culture that C language constructs in 2026 are still 1:1 to Assembly instructions, that pretty much prevails, despite easy proof that isn't the case at various compiler optimization levels. Proficient devs, well many still don't know to distinguish what is their compiler, and what ISO says.
- jstimpfle 4mo agoIt is the case that you can more easily know what happens when you don't use the wrong abstractions but stay in control. Highly-abstracted C++ code basically makes allocations and syscalls in the whitespace between the source code tokens. You can't do systems software like that, you have to roll back the abstractions and roll back the use of pre-canned containers and libraries that you don't understand. So it's all about understanding and control, not about some idea that C was defined in terms of assembly instructions, which it obviously is not. That's a total strawman.
- pjmlp 4mo agoExcept modern C also has plenty of abstractions, devs wrongly assume it doesn't. Then get surprised when it doesn't map to the SIMD/SIMT NUMA machine their code actually executes on.
- gritzko 4mo agoThat is the entire point, yes. Reasoning about layers of completely imaginary entities is what demotivates me about C++ and Rust. Meanwhile, hardware bits are very real (and getting more expensive recently). Having implemented slices and generics in C, now C++ feels like Vietnam flashbacks. https://replicated.wiki/blog/abc https://replicated.wiki/blog/abc
- pjmlp 4mo agoYet C23 isn't K&R C any longer, nor is the hardware a PDP 11. Also when we eventually start talking to agents that perform the whole execution steps by themselves, that is kind of irrelevant. Except for the lucky ones that still code to keep the infrastructure going, which is mostly C++.
- uecker 4mo agoThe "nor is the hardware a PDP 11". Byte access was the main new feature of the PDP 11 that C adopted. Are you saying being able to access individual bytes is not relevant on modern hardware?
- shakna 4mo agoMight more mean that we've standardised on a few things like what a byte even is. The PDP-11 had both 8 and 9-bit bytes. Thats a complexity that few programmers have to touch on, today.
- elch 4mo agoIIRC PDP-11 was a 16 bit word machine with an 8-bit byte. Maybe you remember PDP-10 with 4x9=36 bit words?
- uecker 4mo agoAnyway, I do not see how this affects the design of C in a way that makes no sense anymore today (except that one could require CHAR_BIT to be eight, but there are still DSPs where this is not the case). I think people repeat the "the C design reflects the out-dated PDP-11 hardware" meme because it sounds smart while in reality it is just nonsense.
- wasmperson 4mo ago> C folks still think is a portable Assembly > C [community] wants to still code > many still don't know to distinguish > the culture that... despite easy proof that isn't the case > devs wrongly assume > self inflicted complexity > considered an advantage when argued by C folks > when the same crowd points > as the C crowd pretends it to be You're arguing in this thread not by addressing what people are actually saying but by bringing up some hypothetical version of what "the C Community" thinks, then arguing with that.
- pjmlp 4mo agoI appreciate that restrict isn't there, because it is yet another UB source, programmer knows not to do errors kind of attitude, and secondly no one seems to care enough to write a language proposal for it.
- m_mueller 4mo agoI take it you probably never tried to use any of these languages for HPC. Without a language standard, you have two options there to compile decently performant executables: (1) compiler pragmas, (2) give up and drop to assembly code. I should add here that there's also (3): Switch to Fortran, which made fundamentally different choices and is IMO the only fully supported higher-than-C level language that can produce HPC applications without fighting a compiler left and right.
- einpoklum 4mo agoCan you link to some writeup regarding how Fortran is preferential to C++ (or rather C++ plus compiler `__restrict`) in this respect?
- m_mueller 4mo agoI keep looking around and not finding any, so let me just try here before someone just takes it and slopifies it: * built-in multidimensional arrays with efficient storage. * related to this: built-in array intrinsics ``` real, dimension(100,100) :: A, B, C C = A + B ``` this kind of code is already a close-to optimal "naive" implementation (not considering parallelization). so you start already at a solid place. then you can easily run it in parallel without too much specialized knowledge with OpenMP, OpenACC, MPI or even CUDA. the only thing you really need to be aware of when implementing your own loops/kernels: the intrinsic storage order, to optimize for cache hits. * crucial: all the above amounts to a standard/best practice about how data is structured and formatted. everyone just uses the built-ins. Thus, interoperability between native Fortran numerical libraries is usually a complete non-issue. Meanwhile, Cpp has a fractured ecosystem with different array/vector types for its libraries. Converting between one and the other is usually a no-go. * next, the intent plus pass-by-reference system. it combines IMO the best of both worlds of a functional vs. procedural approach: - I can predefine if an array / variable is intended as input, output or both, simply with `intent(in)`, `intent(out)` etc. - compiler can thus do checks for me if I'm breaking a contract. - yet, since I pass by ref, I don't have to worry about memory not being used efficiently. This really matters once you deal with GB, TB or even PB worth of data going into a simulation over time - there's just no way to deal with that in a purely functional way. - only where really necessary, you can still drop down to pointer semantics, e.g. for the outer glue code of a simulation that swaps outputs and inputs for the next time step. - having `restrict`-by-default semantics here helps immensely. Imagine you have many arrays with lots of data to deal with, and your kernels access always several at a time to do calculations. In Fortran I can write it intuitively, while in C/C++ I must remember to specify `restrict` or to preload all point-inputs first into separate variables to direct the compiler. Otherwise the memory pressure increases, and by far most such physics simulations are memory bandwidth bound - every access counts. * finally, a clean symbol definition system that decouples types from byte lengths. a `float` in fortran is just `real(4)`, a double is `real(8)`, a long int is `integer(8)` and so on. now, it's trivial to do a bit of preprocessing to switch the precision. However, the last part is where Cpp has a strong advantage: Well supported meta-programming (generics, templating or even just well supported pre-processors). Fortran's compilers come with a lot of built-ins, so the lack of these is less of an issue than you might think, but it's still a limiting factor. All that being said, a typical scientist doesn't tend to care and just wants to solve a particular problem rather than thinking in generalized frameworks - and that's why I find Fortran still serves them better for numerics than anything that came since.
- Retr0id 4mo agoDid you use an LLM to write this comment? (I don't mean this as an accusation, I'm uncertain. I'm just trying to calibrate myself.) Edit: I should've had more conviction in my instincts, this is slop.
- DuckConference 4mo agoIt didn't stand out to me on first read but pangram gives it a high confidence rating as being written by AI.
- Retr0id 4mo agoIt's much more obvious when you look at the user's comment history. Curiously my comment above was on +3 karma last time I looked, but now it's on -2. It seems like the median HN user is getting worse at slop detection (or is otherwise ambivalent towards slop comments).
- AlexeyBelov 4mo agoYou were right to call out. I upvoted your comments.
- uecker 4mo agoThe "stronger type system" is mostly a myth in my opinion. It was true in the past in pre-prototype C. The void pointer rules are better in C IMHO as they avoid unneeded casts (that then remove more type safety) and FAMs and variably-modified types can express things C++ simply can't do well.
- flohofwoe 4mo ago> The void pointer rules are better in C IMHO as they avoid unneeded casts ...so much this! A void pointer is an "any-pointer" by design. It shouldn't require casting from and to specific pointer types, that defeats the whole point of having void pointers in the first place.
- TuxSH 4mo ago> It shouldn't require casting from and to specific pointer types You don't need to explicitly cast T* to void* (guaranteed to be safe), you only need to cast when converting out of void*. The rules are basically the same as casting between pointer-to-derived-class and pointer-to-base-class and they make sense.
- uecker 4mo agoThey make sense but reduce type safety, because once you add the cast the case might hide some real typing issue. I sympathize with the idea that the down-cast should be explicit though.
- spacechild1 4mo ago> They make sense but reduce type safety Yes, downcasting can be unsafe and should be used carefully, but what's the alternative? At least in C++ you can't cast between unrelated types without an explicit reinterpret_cast (or C-style cast).
- chrchang523 4mo ago
- flohofwoe 4mo ago> I wrote this after repeatedly seeing experienced C programmers hit the same sharp edges while moving into modern C++ codebases. ...I've seen this more often in the opposite direction. Since C++ is stuck with a ca 1995 non-standard subset of C, C++ coders usually have a very outdated view of C. > I’d also be curious which C constructs people still genuinely miss in modern C++. Not implementing the full C99 designated init feature set was a huge missed opportunity in C++20. Every single feature of C99 designated init is important and clicks with the other features and the rest of the language, take one or two away and it becomes mostly useless (e.g. the order requirement in C++20 means that designated init is only useful for trvial structs). It's especially tragic because Clang already had the full C99 designated init feature set in C++ mode implemented long before C++20 and it worked just fine. > The interesting part to me isn’t "C vs C++," but where the languages diverged philosophically IMHO this "schism" was completely unnecessary and only happened because of ignorance and hubris by the C++ designers. Objective-C shows that C can be extended with radical new features but without messing up the "C side" (e.g. ObjC features don't overlap with C features, which means that ObjC is automatically compatible with the latest C standards). In the end it's not a big deal of course, C and C++ are now entirely different languages and longterm that's for the better. Even the C++ peeps seem to have come to that realization and no longer recommend to "compile C in C++ mode" (like Herb Sutter in 2012 when trying to justify why MSVC had no C99 support: https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-and-c99/ https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...): "We recommend that C developers use the C++ compiler to compile C code (using /TP if the file is named something.c). This is the best choice for using Visual C++ to compile C code." This was bad advice back then and is even worse advice today. At least MSVC got "good enough" C99 support a couple of years later (in VS2015), but after a few hopeful years after 2019 it looks like MSVC development has completely stalled again.
- fuhsnn 4mo ago> IMHO this "schism" was completely unnecessary and only happened because of ignorance and hubris Having attempted to implement it correctly in slimcc, there are indeed some edge cases[1][2] that justify not adapting it fully. [1] Unordered side effects; evaluation of expressions with overlapped destination is implementation defined but not listed as such (the wording in standard is "can potentially not be evaluated"). [2] Both GCC and Clang still get this wrong in 2026: https://github.com/llvm/llvm-project/issues/190858 https://github.com/llvm/llvm-project/issues/190858
- safercplusplus 4mo agoYou might be interested in the scpptool feature to help convert C code to a subset of C that will also compile as C++ (under clang++ at least) [1]. While many of the necessary modifications are fairly trivial, some of them aren't completely so. For example, C++ does not allow `goto`s that would skip over the declaration/initialization of a variable that would be accessible after the jump. So getting the C code to work as C++ can involve some (automatic) code restructuring. Another annoying detail is that C++ doesn't seem to like forward references of `enum`s. That is, while struct A* a_ptr; is fine in both C and C++ even before `struct A` has been defined, apparently enum A* a_ptr; is not cool in C++ until after `enum A` has been defined. One arguable benefit of keeping your C code compatible with (or at least convertible to) C++, is that you can theoretically use scpptool's auto-translation feature as build step to produce memory-safe executables from C code via transpilation to a memory-safe subset of C++. [1] https://github.com/duneroadrunner/SaferCPlusPlus-AutoTranslation2/blob/master/README.md#auto-conversion-of-c-to-valid-c https://github.com/duneroadrunner/SaferCPlusPlus-AutoTransla...
- crest 4mo agoAbout half of them read as "I tried to use C++ as a worse C" e.g. using struct initilisation instead of constructors, using malloc instead of new or new[]. My pet peeve with C++ is that the sequence point operator can be overloaded at which point it stops being a sequence point.
- spacechild1 4mo agoWhat's the "sequence point operator"?
- twoodfin 4mo ago,
- spacechild1 4mo agoThat's the comma operator. I didn't know you could overload it! That's pretty crazy. However, I have never seen anyone do that. Do you have any real world examples?
- addaon 4mo agoSee e.g. the very-popular Eigen library, in which the type CommaInitializer basically exists for the sole purpose of overloading `operator,`, allowing a cleaner matrix initialization syntax. https://gitlab.com/libeigen/eigen/blob/master/Eigen/src/Core/CommaInitializer.h https://gitlab.com/libeigen/eigen/blob/master/Eigen/src/Core...
- spacechild1 4mo agoThanks!
- chabska 4mo agoAs the title indicate, this article is comparing construct-to-construct, not idiomatic code to idiomatic code. You probably won't use struct initialization in C++, yet the feature still exist, so it may be useful to someone to compare it to the similar feature in C.
- acontopo 4mo ago> isn’t "C vs C++," Perhaps be more careful in trying to make LLM output look like you wrote it yourself. The incongruent punctuation mark types, with curly apostrophes and straight double quotes mixed together in the same text, are a dead giveaway.
- AdieuToLogic 4mo agoFrom the article: In 2019 I wrote a short survey of C constructs that do not work in C++. The point was not that C is sloppy or that C++ is superior. The point was that C++ is not a superset of C, and that C programmers crossing the border should know where the checkpoints are. C++ was a superset of C 30-ish years ago. Now, as the author correctly identifies, it is not as both have taken different evolutionary paths.
- pjmlp 4mo agoAlready in C++98 there were differences. ?: has another execution priority. Implicit cast scenarios are reduced in C++.
- electroly 4mo ago30 years ago, in C89 and pre-standard C++, it was the case that `int foo()` in C is a function that accepts any parameters, and in C++ it is a function with no parameters. In C89 you have to write `int foo(void)` if you want no parameters. This counterexample to C++ being a superset of C was well-known even back then. Another well-known counterexample is implicit conversion from void*. In C89 you can do `int* foo = malloc(100);` but in C++ it requires an explicit cast from void* to int*. I don't believe there was ever a time, even pre-standardization, when C++ was a strict superset of C; it always had little incompatibilities here and there.
- avadodin 4mo agoPerhaps in c-with-classes(Cpre)? To the extent that its output could be considered C. It looks like you're right and the answer to when was C++ a superset of C may well be "never". From the description, Cfront had always been a full-fledged parser that only happened to output C since the very beginning.
- zabzonk 4mo ago> a full-fledged parser perhaps more accurately a fully fledged compiler (that emitted C)
- mjs01 4mo agoSome unmentioned incompatibilities I've encountered that makes a C header not directly usable in C++: - C `_Atomic(T)` and C++ `std::atomic<T>`. C++23 has C compatible header `stdatomic.h` that defines `_Atomic(T)`, but it's still problematic - C `_Noreturn/noreturn` and C++ `[[noreturn]]`. C23 `[[noreturn]]` makes them compatible - C inline and C++ inline are different. Good news is their `static inline` are the same - C has anonymous struct. C++ doesn't. Both have anonymous union though
- dhruv3006 4mo ago> restrict: a C promise, not a C++ contract This takes the cake.
- deleted 4mo ago[deleted]
- mike_hock 4mo agoThe thing with the flexible trailing array member is a C++ design flaw. Now the fix wouldn't be to allow those "flexible arrays" in C++, at least not the way C has them, but it should have a concept (not that kind of concept) of types that are indeterminately sized at compile time and whose size is determined at construction. If you're allocating something on the heap anyway, you shouldn't be forced to pay for an indirection in order to have some variable-sized data in the object, you should just be able to put it all in the one allocation. (Sure, you can achieve that with placement new hackery but that certainly isn't "idiomatic" C++.) Of course that's completely incompatible with the way allocation and construction work (storage has to be allocated before the constructor runs). Hence "design flaw" rather than "missing feature."
- mgaunard 4mo agoThere is no problem putting objects past the end of another object in C++. I use this approach as part my zero-copy serialization library for what I call "out-of-line" sequences. It does require smart usage of std::launder to be standards-compliant though.
- p0w3n3d 4mo agoAddress white_house{ .street = "1600 Pennsylvania Avenue NW", .city = "Washington", .state = "District of Columbia", .zip = 20500, }; For me this is the most important initialization in C that helps with clarity so much, I used mostly structs to have function parameters intialized like this However C++ had at time no default initialization for unmentioned fields, so in 2017 I had to remove it when converting the code to C++
- einpoklum 4mo agoDesignated initializers were unofficially supported by GCC (and clang IIANM) since around when C++11 was supported. See: https://godbolt.org/z/3aKaa7dnM https://godbolt.org/z/3aKaa7dnM only if you specifically ask to get an error, would you actually get it.
- deleted 4mo ago[deleted]
- 1718627440 4mo agoYeah you can even create linked lists of static data that way, as you can very well just take the pointer of an object here.
- grougnax 4mo ago[flagged]
- grougnax 4mo ago[flagged]
- Panzerschrek 4mo agorestrict in C++ can't work well. One can mark pointer parameters with this attribute, but in C++ it's not recommended to pass raw pointers, std::string_view or std::span should be used instead, but there is no way to specify restrict for the internal pointer of these containers.
- arcadialeak 4mo ago> std::string_view or std::span should be used instead That is for when the owner is a std::string or an owning range respectively. But a raw pointer does still make sense as a non-owning view over a single element, doesn't it? I'm new to C++ so I might be wrong.
- cocoto 4mo agoNon-owning view over a single element should simply be a reference, you don’t care where this element is located.
- 1718627440 4mo agoThat won't work with NULL.
- Pay08 4mo agoIs there some sort of tool that checks headers for this stuff? On the occasion that I write a C library, I prefer it to be directly usable in C++.
- uecker 4mo agoYou can just run it through a compiler in c++ language mode.
- nulltrace 4mo agoCompiler mode won't catch the `extern "C"` thing though. Both sides compile happily, link blows up on mangled names. What I do is just keep a throwaway .cpp in tests that #includes the header and calls a few of the public functions. Dumb but it's basically the only thing that ever catches that case before some downstream user does.
- cmovq 4mo agoDesignated initializers is one area where C feels much more expressive than C++. And that feature has been standard since C99.
- maxloh 4mo agoI think the problem is that C++ is a poorly designed language with a fundamentally flawed development process. Instead of letting compiler implementers decide which features to add and how to implement them, C++ employs a standards-first, top-down approach. Features are often defined by committee members who may not use modern C++ in their daily workflows, leaving it entirely up to the individual implementations to catch up. Some features were standardized back in 2023, yet not a single implementation supports them in 2026. https://en.cppreference.com/cpp/compiler_support https://en.cppreference.com/cpp/compiler_support https://cppstat.dev/ https://cppstat.dev/
- weinzierl 4mo agoThis is so true and it's a danger for other ecosystems too. Once the big corporations throw their weight in (be it in a committee or only informally) they make their use cases dominant regardless how niche they might be for the rest of the world. For example in Rust there is one big entity that currently pours a lot of energy into improving C++ interop. Now, this is not exactly a niche topic, but especially in a world where AI makes many rewrites possible that we wouldn't have daunted to think about a couple of years before, we shouldn't waste too much effort to save legacy companies enormous codebases at the detriment of our preferred language.
- remix2000 4mo agoRust is impl-first bottom-up and it's stuck with a single implementation and GCC for Rust is still in the works, meanwhile C++26 reflection is already in GCC trunk.
- varispeed 4mo agoMan this table is disturbing. "Same", same as?
- ozgrakkurt 4mo agoYou can pass a flag to clang to allow reordering field initializations in designator initializer thing. It makes the syntax super annoying in case of large structs anyway. It doesn't matter unless you are using constructors or modifying some variables in the initialization expression anyway.
- einpoklum 4mo agoI find variable-length arrays (i.e. arrays whose length is defined at run-time and typically live on the stack) to be kind of dangerous, and try to avoid them, even in C.
- cksk 4mo agoThe array at the end of the struct is not a VLA on the stack. When you allocate the struct, you add enough extra to hold the contents of the array. VLAs on the stack are pretty much universally considered a bad idea, as far as I can tell.
- Findecanor 4mo agoA type of VLA I would like to see more of in C code however, is variable-length array parameters (void func(int n, int arr[n]);) and array pointers (int (*my_array)[n];). Compilers can often do some static bounds-checking of such arrays. But because those features had been introduced together with variable-length array stack variables , people have lumped them all together and thus shunned these features that could otherwise have added safety. BTW, one thing you should never do is use variable-length array declarations and alloca() in the same function. As VLA variables have scope life-time and allocas have function life-time they are not compatible — and allocations could overlap. Yet, not all compilers (/versions) that support both warn when they are used together.
- uecker 4mo agoWhy do you think they are dangerous?
- fanf2 4mo agoIf the VLA size is not controlled it gives an attacker a primitive for arbitrarily shifting the stack pointer. There isn’t any spec for what is a reasonable limit on a VLA size. https://dotat.at/@/2010-01-22-coroutines-in-less-than-20-lines-of-standard-c.html https://dotat.at/@/2010-01-22-coroutines-in-less-than-20-lin...
- uecker 4mo ago
- taylodl 4mo ago[dead]
- deleted 4mo ago[deleted]
- sylware 4mo agoPlease stop using c++. It has a way too complex syntax creating a hard dependency on beyond reasonable "real-life" compilers and runtimes.
- king_geedorah 4mo agoIs there some kind of ambiguity inherent in designated initializers such that the field ordering requirement is necessary for C++ or is it rather just a quirk of the compiler authors’ choices?
- bfrog 4mo agoC++ the good parts is essentially C. If C had the concept of traits and generics or something like them like Rust there’d be no need for C++ and its ideas of mediocre inheritance and macros (templates).
- fluffybucktsnek 4mo agoTbf, I'd say Rust's concepts of Trait Objects and Generics are better implementations (in general) of C++'s inheritance and templates, respectively, rather than entirely different things. I'd also add constexpr/const blocks to good ideas to add, or else generics/templates will be abused (yet again).
- bfrog 4mo agoThere is no inherit in Rust. If you choose to try and do OOP horror Rust makes this fairly painful
- fluffybucktsnek 4mo agoI know. I mentioned C++ inheritance because it's its default dynamic dispatch mechanism that I'm aware of.