Polymorphic Types in C [pdf]
open-std.org
open-std.org
>Q: Why does C3 require that types start with upper case but functions with lower case?
Hard pass.
> Avoid "big ideas".
Basically you can do something like this in c :
var value;
value = data(100);
// or
value = data(0, 2, 3, 4, 5, 6);
``` Using a run-time value of type _Type T in _Var(T) can be allowed in general (and is useful), but needs to be restricted to contexts where full type information is not required at compile-time. ```
Semantic rules that conditionally apply only in some contexts is a common tendency of the C++ standard that many C programmers often dislike.
(a) it is desired that _Var(T)* and void* be compatible;
(b) however. pointers-to-T for different T are not guaranteed to be compatible in general,
as a consequence, it is NOT guaranteed that _Var(T)* is compatible with T*, which is a theoretical wart worthy of C++. I can see it becoming especially annoying if you’re trying to introduce _Var(T) polymorphism into a code base that currently uses preprocessor macros that textually substitute types.
Perhaps the better solution would be forgo giving void* any special status whatsoever. However, that means that this type of polymorphism can’t be implemented using void* as a polyfill.
It's worth keeping in mind that code aesthetics is an important aspect of C codebases. There's a lot of C code that is exclusively lowercase, sans the macros. So introducing keywords like _Type and _Var will serve to hinder their adoption, because it'd make the code that much more "ugly". Just like what happened with _Generic - a reasonable feature, bad keyword selection -> barely any field use.
Typically, a <stdkeyword.h> header is included that contain macros to provide the lowercased variants. I.E., this is how _Bool was implemented; <stdbool.h> provides the lowercased `bool` variant.
You can't easily lowercase _Type and _Var, so practically speaking it will take years before these features could be suitable for wide-spread adoption. Hence my original comment - given the friction, is it worth expanding the language this way at all then?
Just as with stdbool.h before, there could be a stdlib header which wraps those internal names into something more human-friendly.
Writing code by hand, it's quite unwieldy, since the compiler doesn't know that a `FinalFooFrobber` always contains has its vtable set to `FrobberVtable foo_vtable`, which often appears only after inlining.
Asking C programmers to do inlining by hand is not sane. This isn't like interpreted languages where inlining sucks - here, the compiler is quite capable of it, just the vtable stuff confuses it.
C++ started as a reasonable extension to C, but now it's just something else (nor does it even want to be seen related to C now anyway). Extending C with something new, without rushing, is a perfectly fine idea.
int i;
int *ip;
typeof(i) *i;
_Var(_Typeof(i)) *i;
What? How are "int i" and "int *ip" equivalent?_Var(_Typeof(i)) ident strikes me as incredibly silly. The type value produced by _Typeof(i) should be directly usable as a declaration specifier, without requiring hoisting to a name.
FFS, GNU C has had a sane typeof working for years.
To bind a type to an identifier, we don't need a _Type type at all. typedef does this!
I.e. not:
_Type x = int;
but typedef int x;
Existing, decades-old typedef is how you bind an identifier to a compile-time type value. There is no need to do this via a _Type type specifer, which then forces us to move the type we want into the initializer. We use the typeof storage class specifier, which then lets us have the type we want as another specifier, and we don't need an initializer.This works in GNU C:
int x;
typedef typeof(x) tx; // same as typedef int tx;
The problem of declared, named containers to capture type is long solved.The only thing I'm confused about is why more C standards keep coming out. C is what it is. Work with the archaic parts of it as need be. You use C for ultimate cross-platform compatibility (every exotic platform and its mother has an ANSI-C/C99 compiler). If you're able to run on the most bleeding-edge C compiler supporting the most recent WG version of C... then why not just use another language? I say this as someone who loves C. If there are bits that feel old, you just create a DSL to get around these problems that compiles down to C... but you don't change C itself.. Just my perception working with all this for many years now.
A trivial example is assigning pointers to incompatible types or passing them to incompatible function parameters not being an error. GCC 14 and Clang 16 are only just now making these errors by default. C++ never had this problem.
Another example, is having type safe generic functions and classes. Using void* to pass around arbitrary types is something that you don't need to do in C++ because it supports generic programming.
In C++ you can for example, pass an array of a specific length to a function, and make it a compile time error to pass an array of incorrect length, using std::array. In C arrays decay to pointers.
static_case is also a thing in C++ that allows you to cast between types much more safely.
There are so many things that I could probably spend several hours typing them out and I don't think that is really needed.
C++ is certainly more type safe than C, whether or not you think the tradeoffs it makes are worth is something to debate for sure, but it gives you tools to write type safe code that simply don't exist in C.
https://wiki.gentoo.org/wiki/Modern_C_porting#What_changed.3...
So in summary, I think this just confirms that C++ people think it is safer because they do not know how you would do this in modern C.
Much of the existing C code out there is filled with these things because they weren't errors until recently, and people ignore warnings. This is why Fedora and Gentoo are doing so much work to port all of their software to "modern C", so that it will actually compile with new compilers, and hopefully fix bugs at the same time. Existing c++ code did not have this problem because it was always an error.
Note that modern compiler in this case means bleeding edge, released literally within weeks or months of this posts date, most distros are not yet compiling the world with these and most users are not using them.
https://fedoraproject.org/wiki/Changes/PortingToModernC https://wiki.gentoo.org/wiki/Modern_C_porting
>Passing void* around is also generally not necessary in C.
This is not true.
I know several examples in real libraries that require this, two OTTOMH is PAM and Wayland. Both of these require void pointers to give you access to some state object that you create and need passed around, it has no way to know what this type will be ahead of time, and C has no other way to do this.
Another example is data structures or algorithms on data structures like sorting, you either use macros to emulate generics or you use void pointers. Qsort is a perfect example.
>One can take the address of an array and then get the same type checking.
I don't think this is true, C will not include the length in the type. Passing std::array<T, N> in C++ causes an error if T or N are different, in C this length information is not part of the type and does not get type checked.
Here is the array example: https://godbolt.org/z/P8494W3Pf Note how grotesquely bad the C++ syntax is for exactly the same type safety.
> I don't think this is true, C will not include the length in the type.
No, you can take the address of an array of length N, as long as you're content with having a function that requires the array be that particular length. For example, if you wanted to write a function that adds one to every element of an array with ten elements, and require that it must have ten elements, you can write it like this:
void add_one(int (*array)[10]) {
size_t i = 0;
do {
(*array)[i] += 1;
} while (++i < 10);
}
However, you usually don't see people do this, because they want their functions to work on all arrays, regardless of their length. So, they pass a pointer to the first element and a value representing the length.You will see people pass pointers to arrays when writing functions that operate on 2D arrays, because that's how you pass a pointer to the first element of a 2D array. The address of the first element of an array of size 10 arrays of int, is a pointer to a size 10 array of int.
// 2D_array_ptr: a pointer to the first element of an (n x 10) 2D array
void add_one_to_array_of_array10(int (*2D_array_ptr)[10], size_t n) {
for (size_t i = 0; i < n; ++i) {
for (size_t j = 0; j < 10; ++j)
array[i][j] += 1;
}
}
In function signatures, array declarations decay to be equivalent to pointers, so you could also write the function signature like this: void add_one_to_array_of_array10(int 2D_array_ptr[][10], size_t n)Getting features into WG21 still requires some effort to get them through, regardless of what people in the outside think, and yeah I do agree it could get some more direction.
C++ may seem to get everything dumpped into it, however any language nerd that feels like diving into what the history of languages with similar age (Python, Perl, Ada, C#, Java, F#, OCaml,...) have across all their versions, standard library, main interpreter/compilers, .... will find out those aren't much better either.
Second, no, g++ does act identical to the c compiler component of GCC.
Now there are languages like Odin, Zig and C3 that are trying to fill that niche but having C evolve slowly to accommodate such users is also a great idea.
C99 added Designated Initializers which IMHO is probably the most innovative declarative syntax to initialize anything and it took C++ roughly 20 years to adopt such a thing (and then they had to nerf it).
Tangentially related, the macro-based containers you've written here [1] are the best answer for type-generic containers I've come across. One "gotcha" is the container name must be a valid C identifier otherwise it doesn't token paste correctly (see Example #2 of your REAMDE where you typedef'd string* as string_ptr to workaround this). Would you give consideration to a new preprocessor mechanism for concatenating a list of tokens into a single valid C identifier? i.e. Something like CONCAT(struct Foo *) would produce struct_Foo_Ptr? The result is guaranteed token paste-able.
#define foo(tag, T) struct tag { _Var(T)* p; }
then we also need a solution for the problem you mention. One option I thought about is to allow strings as tags: https://godbolt.org/z/cMc3aPjsK And if the builtin that transforms types to strings you be used as a string literal, this could work nicely. But even better may be to just all parametrizing the tag with a type: struct foo(T)
I am still thinking about this.
I don’t understand your second point.
It is not C's job to be everything to everyone.
Is it C89 or gnuc89? gnuc89 is the one that allows // for single line comments, I don't know what other features gnuc89 adds over c89.
I also call C89 ANSI C :)
Are you guys using gcc or clang?
We need a "C++ lite".
We do this by moving features from C++ to C, bit-by-bits, until C become bloated.
It this point, if I want generics, I'd rather just use C++.
void foo(int X, int Y) { double a[X][Y]; .. }
In fact, this is one reason I switched away from C++, because arrays are so bad in C++.
(yes, one can activate library assertions, but still - by default - unsafe)
... is somewhat understatement.
At least in C there's a possibility to learn from C++'s mistakes, and only integrate the 'good parts' (and the C++ template system certainly isn't one of those good parts).
In any case, if you need a clean subset of C++ (but nobody I have met really agreed about what this is) nobody is stopping you from defining one.
Overall? Without a doubt. But these specific features they've been adding are implemented very unnecessarily strangely just to avoid fully committing to them, so that they end up in this design space where they may not be weirder than the full Lovecraftian horror of the equivalent C++ feature, but they are much weirder than the ideal/abstract concept of the equivalent C++ feature, which is what I wish they would have pulled in. Like, looking at this proposal, it isn't clear to me at all how the compiler is actually compiling or the runtime is executing these polymorphic function calls at all, in cases where they don't just collapse to void*. And it isn't clear to me how exactly type scoping and substitution works either. Whereas in concept at least templates are crystal clear and also easy to expand to do much more advanced macro-less metaprogramming, like D does.
> and often think some parts are weird simply because they are different.
I have way more experience with C than C++, but I'm usually a Rust programmer which works more like C++ so maybe that's where my bias is, but I really think this implementation is unnecessarily weird in general too — I've used a lot of languages.
To me it is crystal clear how this will work, and I much prefer this to generics in C++ and Rust or other similar languages.
But I am not saying it is wrong what C++ and Rust does. So if you prefer this, those languages are there ready to be used. I just think there should also be a good alternative for people who do not like how this works there.
That's what Rust looked like in the beginning - a better C++. But that seems to be getting corrupted by a bunch of language designers too now.
Showing up at WG21 is much more appealing to most folks.
Maybe compare how C23 compares to C89, and future roadmap.
...and let's just hope it stays that way ;)
Also, Ada has a very decent interop with C. To the point that w/o any prior experience, I needed to use some functions from SQLite that weren't already exposed through Ada, and it was a pretty smooth sailing: very little "glue" code, all code that connected C and Ada was written in Ada.
Personally I am not a big fan, because it doesn't have a good story for use-after-free (other than adopting similar analyser tooling like C derived languages), if I want to type @ all over the place I can use Objective-C, and the community doesn't seem very keen in supporting binary library distribution.
That is me, we don't have to like all the same things.
Even with UAF caveat, it still better than plain old C.
Personally I find Odin more appealing than Zig.