With the release of C23 and its palette of drastic additions I've noticed how C as a programming language is going to shit in modern versions. Here is what I mean:
Constexpr
Since the dawn of time the way we store const compile time values in C has been with macros:
#define MAX_ENTITIES 4096
Basic and straightforward. C++ has had an additional method of doing compile time variable
declaration with its constexpr keyword. You can define constants that get evaluated
at compile time like this:
constexpr int MAX_ENTITIES = 1024 * 4;
As you can see this allows us to have actual arithmetic done at compile time before the constant is used. May seem cool but you can do the same to macros by just doing the math in your head or using a calculator ahead of time, so it's kinda useless for constant variables.
Where I think constexpr really shines is at function evaluation (which is mostly done
with consteval in C++20 and higher). Let's say we had an asset manager with a get-asset
function that needs to do both a hash and a lookup into a map by receiving a constant string:
Sprite assets_get_sprite(const char *const name);
In order to find the sprite in our sprite map, where each key is a string and each value is a
Sprite, this function needs to rehash the string every time AND do the lookup. Using
constexpr we can omit the hashing stage entirely and have it processed at compile time.
// should be consteval in C++20 or later
constexpr uint32_t assets_hash_name(const char* name)
{
uint32_t hash = 2166136261u;
while (*name)
{
hash ^= static_cast<uint8_t>(*name++);
hash *= 16777619u;
}
return hash;
}
Sprite assets_get_sprite_from_hash(uint32_t hash)
{
return my_lookup(hash);
}
#define assets_get_sprite(name) assets_get_sprite_from_hash(assets_hash_name(name))
As most of us may know, C23 introduced the constexpr keyword. The formerly C++-only
feature has been added to the C programming language. Or has it?
Let's try to run this code:
#include <stdio.h>
int main(void)
{
constexpr int test = 67;
printf("%i\n", test);
return 0;
}
clang main.c -o main.exe -std=c23
67
Nice, it returns what is expected.
Now let's make a constexpr func—
main.c:1:1: error: 'constexpr' can only be used in variable declarations
1 | constexpr uint32_t assets_hash_name(const char* name)
Oh... Well... So constexpr is pretty much useless. WTF IS THE POINT C23!!!
Default Values
As we all know, to this day C does not have any builtin features for default parameters or default struct field values.
C++ has had this issue fixed a long time ago, allowing us to do stuff like this:
void renderer_draw_rect(Vec2 position, Vec2 size, Vec2 pivot = Vec2{0.5f, 0.5f}, Color color = COLOR_WHITE);
struct Player
{
const char *name = "Bob";
int health = 100;
};
This has been one of the most requested features in C and yet it hasn't been provided, and is the main reason why I consider C-styled C++ to be the goat.
Generics
Hey, do me a favor. Can you tell me what this code does?
template <std::size_t... Is>
struct __variant_idx_seq_impl {};
template <typename T, T... Is>
using __idx_seq = std::integer_sequence<T, Is...>;
template <std::size_t Np, typename Tp, typename... Ts>
struct __get_nth_type : __get_nth_type<Np - 1, Ts...> {};
template <typename Tp, typename... Ts>
struct __get_nth_type<0, Tp, Ts...> { using type = Tp; };
template <std::size_t Np, typename... Ts>
using __nth_type_t = typename __get_nth_type<Np, Ts...>::type;
template <typename Fp, typename Vp, std::size_t... Is>
constexpr decltype(auto) __visit_impl(Fp&& __f, Vp&& __v, __idx_seq<std::size_t, Is...>)
{
using __ret_t = std::common_type_t<
decltype(std::forward<Fp>(__f)(std::get<Is>(std::forward<Vp>(__v))))...>;
constexpr __ret_t (*__vtable[])(Fp&&, Vp&&) = {
+[](Fp&& __f, Vp&& __v) -> __ret_t {
return std::forward<Fp>(__f)(std::get<Is>(std::forward<Vp>(__v)));
}...
};
return __vtable[__v.index()](std::forward<Fp>(__f), std::forward<Vp>(__v));
}
template <typename Fp, typename... Vps>
constexpr decltype(auto) __variant_visit(Fp&& __f, Vps&&... __vs)
{
return __visit_impl(
std::forward<Fp>(__f),
std::forward_as_tuple(std::forward<Vps>(__vs)...),
std::make_index_sequence<
std::common_type_t<std::integral_constant<
std::size_t, std::variant_size_v<std::remove_reference_t<Vps>>>...>::value>{});
}
Yeah, I can't tell you either.
However this IS valid C++ code somehow. Not even Microsoft can write code this bad (I think).
So where did everything go wrong? Well, generics are supposed to be something incredible. You can execute the same code for different types without needing to write specific code for each separate case. Static polymorphism is the best polymorphism in my opinion.
However, that simplicity doesn't last long. The second someone needs a bit more flexibility, they reach for SFINAE, concepts, variadic packs, or specialization chains — and readable code turns into the abomination you saw above.
The problem is templates were never really designed as a generics system. They're glorified text substitution that got bent into doing generic programming, type computation, and metaprogramming all at once. So yeah, you can do anything with them, but only if you're okay writing code that looks like a stack trace threw up.
So why should we even bother with templates to begin with?
Well, as long as we stick to templates in the most minimal way possible and keep them slim without caring too much about safety or compatibility, we can have simple, elegant, and reusable code:
template <typename T>
struct List {
T* data;
int size;
int capacity;
};
template <typename T>
void list_push(List<T> *list, T value)
{
// logic to push to the list and resize the buffer if needed
}
End?
C23 had a real chance to drag C into the modern era, and instead it gave us half-features that
crumble the moment you push on them — constexpr that can't touch functions, still no
default values, and a neighboring C++ ecosystem where "generics" means willingly stepping into
template hell. If you want the good parts of C++ without the metaprogramming nightmare, write
C-style C++: minimal templates, no STL gymnastics, no SFINAE séances. Skip whatever C23 is trying
to be, and skip idiomatic modern C++ while you're at it.