C constructs that still don't work in C++

jalospinoso1 pts0 comments

C Constructs That Still Don’t Work in C++ — and a Few That Changed | Josh Lospinoso Skip to content Blog | May 20, 2026<br>C Constructs That Still Don’t Work in C++ — and a Few That Changed<br>A 2026 sequel to C Constructs That Don't Work in C++: what still breaks, what C++20 changed, and what C23 changed.<br>GitHubJLospinoso/c-constructs-still-dont-work-cpp<br>In 2019 I wrote a short survey of C constructs that do not work in<br>C++.<br>The point was not that C is sloppy or that C++ is superior. The point was that<br>C++ is not a superset of C, and that C programmers crossing the border should<br>know where the checkpoints are.

That advice still holds. But the border moved.

C++20 picked up a version of designated initializers. C++20 also repaired some<br>low-level object-lifetime cases around malloc that used to be easy to<br>describe incorrectly. C23, meanwhile, changed the old empty-parameter-list rule<br>that made void f() mean something dangerously different in C than in C++.

The practical lesson is the same, but sharper: when you discuss C/C++<br>compatibility, label the language mode. “Valid C” and “valid C++” are not<br>precise enough anymore. You often need to say C17, C23, C++17, C++20, or<br>C++23.

I also put the examples behind this post in a small<br>companion repository.<br>The repository is for repeatable checks; its<br>Compiler Explorer links<br>are for quick diagnostics.

Compatibility matrix

Here is the short map. Details follow. The point is not just that C++ is not a<br>superset of C. The point is that some of the canonical examples people still<br>repeat changed under C++20 or C23, so the correct answer now depends on the<br>language mode. This table is a map, not a substitute for the examples; on<br>narrow screens, the sections below are easier to read than the full matrix.

StatusConstructC17C23C++17C++20 / C++23Practical adviceStill differentvoid* to object pointerImplicit conversion from malloc is idiomatic C.Same.No implicit conversion.Same.In C++, do not make malloc your default allocation strategy. If you must use it, cast deliberately and handle lifetime deliberately.Changed since 2019malloc and object lifetimeC allocation creates storage used as objects by C’s rules.Same broad C model.Easy to write code that compiles with a cast but has no C++ object lifetime.Some implicit-lifetime cases are repaired. Constructors still are not called.Distinguish storage, lifetime, initialization, ownership, and destruction.Still differentDiscarding constConstraint violation; compilers often warn.Same basic concern.Ill-formed without a cast.Same.A cast may compile. It does not make writes to actually-const objects defined.Changed in C23EnumsEnumerator constants are integer-like; enum objects convert freely enough to surprise C++ programmers.C23 adds fixed underlying types and more explicit typing rules for enumerators.Enum types are distinct; int to enum is not implicit.Same; enum class remains stricter.Use enum class for C++ APIs. Use plain enums only when ABI or C interop demands it.Changed in C23void f()No prototype in the old sense; mismatched calls may compile, but are not defined.Behaves as though declared with void.Means no parameters.Same.For shared headers, still write void f(void) in C-facing APIs unless you control the language mode.Changed since 2019Designated initializersFull C-style designated initialization, including out-of-order, array, nested, and mixed forms.Same family, with C23 evolution elsewhere.Not standard C++.Standard, but narrower than C.Useful in C++20, but only for aggregates, direct members, declaration order, and all-designated clauses.Extension traprestrictStandard C99 qualifier.Still standard C, with C23 wording updates.Not standard C++.Not standard C++.Use compiler extensions only behind a portability boundary.Still differentFlexible array membersStandard C99 trailing-array pattern.Still standard C.Not standard C++.Not standard C++.Keep the C layout at the ABI edge; translate into span, vector, or an explicit header/payload representation.<br>Designated initializers: yes, but not C’s version

The 2019 post said designated initializers were not available in C++, with a<br>note that they were likely coming in C++20. That note aged well.

C++20 added designated initializers for aggregate initialization. This is valid<br>C++20:

struct Address {<br>const char* street;<br>const char* city;<br>const char* state;<br>int zip;<br>};

Address white_house{<br>.street = "1600 Pennsylvania Avenue NW",<br>.city = "Washington",<br>.state = "District of Columbia",<br>.zip = 20500,<br>};<br>This is not the same feature C programmers are used to.

C++ designators must name direct non-static data members, and they must appear<br>in declaration order. That means this out-of-order form remains invalid C++:

struct Options {<br>int timeout_ms;<br>bool verbose = false;<br>int retries = 0;<br>};

Options o{<br>.retries = 3, // invalid C++20: out of declaration order<br>.timeout_ms = 5000,<br>};<br>C also permits patterns that C++ still rejects, including array designators and<br>nested designators:

int table[4] =...

still changed standard constructs work designated

Related Articles