https://gitlab.com/nbdkit/nbdkit/-/blob/296b9dd041fdbcbaa731...
(My definition of a "cast wrapper" is: function body contains exactly one function call, mostly forwarding arguments. Pointer offsets¹ on arguments/retval are allowed, as well as adding sizeof/offsetof/constant integer arguments, but nothing else.)
¹ this may include some conditionals to keep NULL as NULL when applying a pointer offset.
I would expect the compiler to be able to optimize these away in 99% of cases, but TBH I don't care if it doesn't and I'm littering a bunch of small wrapper functions.
#define VECTOR_TYPE int #include “vector.h”
This will let you step into the macro generated functions in GDB and doesn’t require putting the function creation in macros (no backslashes, can write the implementations like normal code)
You can also get syntax like this that way Vector_Push(int)(&vec, 1);
Which imo does a better job distinguishing the type from the name
TBH, rather than fancy high-meta generic support, I'd be much more appreciative of ISO C adding something like "_Include" to the preprocessor, e.g.
#define DEFINE_MY_WRAPPER(foo, bar) _Include("wrapper.h")
/* "foo", "bar" remain accessible in wrapper.h */
I tried implementing this in GCC but didn't get very far, it runs a bit counter to how includes are handled currently.P.S.: I ended up doing this https://github.com/FRRouting/frr/pull/15876/files#diff-90783... instead; a tool that can be invoked to expand some DEFINE_FOO if needed for debugging. It's not used particularly often, though.
In zig, genericMax([]i32) would emit seperate code to genericMax([]i16). This proposal has both of those functions backed by the same symbol and machine code but it requires a bunch of extra arguments, virtual function calls, and manualy offsetting indices into arrays. You could use zig to do the same with some effort.
fn genericReduceTypeErased(
size: usize,
total: [*]u8,
list: []const u8,
reducer: *const fn(total: [*]u8, current: [*]const u8) callconv(.C) void,
) void {
for(0..@divExact(list.len, size)) |i| {
const itm = &list[i \* size];
reducer(total, @ptrCast(itm));
}
}
inline fn genericReduce(
comptime T: type,
total: *T,
list: []const T,
reducer: *const fn(total: *T, current: *const T) callconv(.C) void,
) void {
genericReduceTypeErased( @sizeOf(T), @ptrCast(total), std.mem.sliceAsBytes(list), @ptrCast(reducer) );
}
The proposal is basically syntax sugar for making a typed version of the first function in CThe point is not to use something else, but to improve what they already use
I'm not convinced this specific proposal does that, but that's a separate matter. JenHeyd Meneide has a Technical Specification paper for a defer statement in C, which is visibly based on Zig's version, and I applaud that.
https://thephd.dev/just-put-raii-in-c-bro-please-bro-just-on...
e.g. GNU make could do very much with some better abstractions (not sure about this one specifically) and yet one cannot use such things until the last VMS user of gmake has got that feature in their C compiler.
IMO that probably means the most effective way to modernise C programs might be to transpile the modernisations to e.g. C89. That could be done on any device and the result, one hopes, would work on all the systems out there which might not have the modern compiler on them.
I mean, most of C just compiles as C++ and does the right thing. There may be some caveats with UB (but less if you don't switch compiler vendor) and minor syntactic differences, but I guess most of them could be statically checked.
Or is there any other (performance/size) reason to not do this?
um, not unless explicitly written to so.
In my experience, unless you’re strictly talking about header files (which I’m assuming you’re not), C code compiling via C++ compilers is usually hit or miss and highly dependent on the author of the C code having put in some effort into making sure the code actually compiles properly via a C++ compiler.
In the overwhelming majority of cases, what lots of folks do is just slap that `extern "C"` in between `#ifdef __cplusplus` and call it a day—that may work for most forward declarations in header files, but that’s absolutely not enough for most C source files.
By the way, a great example of C code that does this exceptionally well is Daan Leijen’s awesome mimalloc [0]—you can compile it via a C or C++ compiler and it simply just works. It’s been a while since I read the code, but when I did, it was immediately obvious to me that the code was written with that type of compatibility in mind.
IMO there's no point even attempting a blind recompile of C as C++. A transpiler tool would be needed.
If you did that and implemented marking passing size info over function calls then you could probably mechanically convert crappy foo(void *buf, int sz) code to foo(slice_t buf) which would be a lot safer to maintain.
You're asking about old C code - that code won't compile as C++. And newer C code would likely also be cleaner and not benefit much from being compiled as C++.
(And, C++ is a net negative over C anyway due to much poorer reviewability.)
C is sufficient. There doesn't seem to be anything left that you can do without making it "Not C". Its ABI is the de facto standard.
I would rather all the energy go to making a language without the legacy requirements. Programming languages develop very slowly because they are bound by social uptake. Putting C specifically as legacy would help accelerate those timelines.
The intent of maintaining C is to help maintain the shittons of existing C code. Even if people were to immediately start rewriting everything, it'd take a decade or two until most things have a replacement. And you still need to maintain and update things until then. There's enough work to think about making that task better - especially for safety concerns, like we're talking about here.
(And, yeah, sunk cost and whatnot… especially if you're making a commercial product, a rewrite is a hard sell and a lot of C code will live far longer than we all wish. It'll be the COBOL of 2077, I suspect.)
¹ "high-level" C code has a lot of other options and probably shouldn't have been written in C to begin with. For low-level C code, Rust is the most suitable² and popular option, everything else is significantly behind (in popularity/usage at least). That might of course change, but for the time being, it is Rust and Rust is it³.
² this doesn't mean Rust is specifically targeted at or limited to low-level things, it just does them quite well and is a good slot-in for previous C uses.
³ of course C++ was supposed to be "it" the entire time, but at least as far as I'm personally concerned, C++ is a worse disaster than C and needs to be replaced even more urgently.
⁴ I'm aware Zig exists. Call me when it reaches some notable fraction (let's say ⅓) of Rust's ratings on the TIOBE index.
Call me when I can integrate Rust into my build system without Cargo. Or when the Rust standard libary handles out of memory correctly. Or when compile times are within a factor of 2 of C. Or when debug mode isn't an order of magnitude or more slower to execute. Or ...
Rust ain't gonna displace C any time soon. For C++, you might have a better argument.
That said, I think some of your expectations are rather personal to you with limited objective merit: the Linux kernel Rust people seem to be happy to use Rust without its stdlib to get the panic-freedom they need, and compile times being slower is something that needs to be weighted against the safety guarantees you get in exchange. Rust intrinsically warns/errors on a lot of things that with C you need a static analysis build for — and if you compare build time against C+SA, it's actually not slow.
Incrementally replacing C code with Rust is far, far easier than C++. For instance, passing null-terminated strings to Rust code is quite straightforward, whereas a std::string absolutely is not.
C will live for a very long time, but C++ will live even longer. Maybe that sounds absurd, but the fact is that every major C compiler is written in C++, and the rest of the toolchain is moving that direction if it isn't already there.
And yet other languages manage to do that far better than Rust.
Interfacing to C from Zig is both pleasant and straightforward, for example.
and the ecosystem is very java and JavaScript like. it's impossible to import any dependency today and cross compile without a lot of troubles.
so yes, c things are being ported to rust, but in situations they would have been ported to cpp.
Not quite. I doubt librsvg would have been ported to C++ if Rust didn't exist. Nor do I believe that C++ would have been allowed into the Linux kernel as readily as Rust currently is, e.g. Android's binder.
Sure, Rust is a small language, but it is making consistent progress and it is only becoming more of a viable C replacement. Of course, C isn't going anywhere and the vast majority of C code is not going to be rewritten in Rust. However, that is normal in the lifecycle of programming languages, i.e. codebases are not usually rewritten, they simply become obsolete and are replaced. Just like Java didn't need to replace every line COBOL to become the enterprise language of choice, Rust doesn't need to replace C everywhere to become the preferred systems programming language.
If I pass (int) and (void ) at different places, they will convert to void *, but won't be accepted in the new code.
Still, it would be great to have an implementation where it can be an optional error, and see what it finds.
That's not true. Go's generics are monomorphized, when it makes sense (different shapes) and not monomorphized when it doesn't (same shapes). It's a hybrid approach to combine the best of both worlds: https://go.googlesource.com/proposal/+/refs/heads/master/des...
C++ fully monomorphizes class/function templates, and therefore incurs 0 overhead while unlocking a ton of inlining, unlike Go.
For example Rust's name.contains(|c| c == 'x' || (c >= '0' && c <= '9')) gets you code which just checks whether there are any ASCII digits or a lowercase latin x in name. If you don't have monomorphisation this ends up with a function call overhead for every single character in name because the "shape" of this callable is the same as that of every callable and a single implementation has to call here.
Is Go's choice a valid one? Yes. Is it somehow the "best of all worlds"? Not even close.
Edit: I think the Go compiler might actually be smart enough to inline in this kind of case. See line 113 of the assembly listing here: https://godbolt.org/z/8qGdzeKcG
Do you have an example of a "good modern C compiler" which inlines the function? Presumably this is a magic special case for say, qsort ?
The broader point is that compilers (including the Go compiler, it seems) can and do make this kind of optimization via inlining, and that this doesn't have much to do with any particular approach to generics.
We got into this with the claim (never substantiated) that this works for the C library function qsort, which is notable because it does indeed work for the C++ and Rust standard library sorts which are done the way I described.
It does not surprise me that Rust inlines more aggressively than Go, but that doesn't have anything much to do with differences between Rust and Go generics or type systems.
Adopting the type-level fiction that every lambda has a unique type does not fundamentally make inlining any easier for the compiler. It may well be that in Rust the monomorphization triggered by this feature of the type system leads to more inlining than would otherwise happen. But that is just an idiosyncrasy of Rust. Compilers can do whatever kind of inlining they like regardless of whether or not they think that every anonymous function has its own unique type.
Edit: One further thing that's worth noting here is that the Go compiler does do cross-module inlining. So (unlike in the case of a typical C compiler) it is not the case that the kind of inlining we've been talking about here can only occur if the function and the lambda exist in the same module or source file. https://utcc.utoronto.ca/~cks/space/blog/programming/GoInlin...
What Go developers wanted was a solution to the problem of having to repeat things like simple algorithms (see e.g. slice tricks) once for each type they would ever need to operate on, and also a solution to the problem of confusing interfaces dancing around the lack of generics (see e.g. the standard sort package.) Go generics solve that and only that.
Every programming language has to choose how to balance and prioritize values when making design decisions. Go favors simplicity over expressiveness and also seems to prioritize keeping compile-time speeds fast over maximizing runtime performance, and their generics design is pretty consistent with that.
The Go generics design did not win over anyone who was already a Go hater because if you already didn't like Go it's probably safe to say that the things Go values most are not the things that you value most. Yes, everyone likes faster compile times, but many people would prefer faster runtime performance. Personally I think that it's worth balancing runtime performance with fast compile times, as I prefer to keep the edit-compile-test latency very low; that Go can compile and run thousands of practical tests in a couple seconds is pretty useful for me as a developer. But on the other hand, you may feel that it's silly to optimize for fast compile times as software will be ran many more times than it will be compiled, so it makes sense to pay more up front. I know some people will treat this like there's an objectively correct answer, even though it's pretty clear there is not. (Even calculating whether it makes sense to pay more up front is really, really hard. It is not guaranteed.)
So actually, I would like type erasure generics in C. I still write some C code here and there and I hate reaching into C++ just for templates. C is so nice for implementing basic data structures and yet so terrible for actually using them and just this tiny little improvement would make a massive, massive difference. Does it give you the sheer power that C++ templates give you? Well, no, but it's a huge quality of life improvement that meshes very well with the values that C already has, so while it won't win over people who don't like C, it will improve the life of people who already do, and I think that's a better way to evolve a language.
What are you referring to here? Code like
func PrintAnything[T Stringer](item T) {
fmt.Println(item.String())
}
looks like type-safe duck typing to me. auto foo(auto& x) { return x.y; }
The equivalent Go code would be package main
import "fmt"
func foo[T any, V any](x T) V {
return x.y
}
type X struct {
y int
}
func main() {
xx := X{3}
fmt.Println(foo[*X, int](&xx))
}
which does not compile because T (i.e. any) does not contain a field called y. That is not duck typing, the Go compiler does not substitute T with *X in foo's definition like a C++ compiler would.Not to mention Go's generics utterly lack metaprogramming too. I understand that's almost like a design decision, but regardless it's a big part of why people use templates in C++.
func foo[T any, V any](x T) V {
return x.y
}
would also not fly there, because T and V are not usefully constrained to anything. Go is the same then. I prefer that model, as it makes local reasoning that much more robust. The C++ approach is surprising to me, never would have thought that's possible. It seems very magic. package main
import "fmt"
type Yer[T any] interface {
Y() T
}
func foo[V any, X Yer[V]](x X) V {
return x.Y()
}
type X struct {
y int
}
func (x X) Y() int { return x.y }
func main() {
xx := X{3}
fmt.Println(foo(&xx))
}
package main
import "fmt"
func foo(x *X) int {
return x.y
}
type X struct {
y int
}
func main() {
xx := X{3}
fmt.Println(foo(&xx))
}
Now, gc will give you two different sets of instructions from these two different programs. I expect that is what you are really trying and failing to say, but that is not something about Go. Go allows devirualizing and monomorphizing of the former program just fine. An implementation may choose not to, but the same can be said for C++. Correct me if I'm wrong, but from what I recall devirtualization/monomorphization is not a requirement of C++ any more than it is of Go. It is left to the discretion of the implementer. func foo[V any, X Yer[V]](x X) V
can be called with only one type (X) and therefore manages to emit the same code in main_main_pc0. It all falls apart when you add a second struct which satisfies Yer [1], which leads the compiler to emit a virtual function table instead. You can see it in the following instructions in the code with a second implementation for Yer added: LEAQ main..dict.foo[int,*main.X](SB), DX
LEAQ main.(*X).Y(SB), CX
CALL CX
[1] https://godbolt.org/z/zTco4ardxThe intent was for you to try it in other implementations to see how they optimize the code.
The term you are looking for is structural typing.
For a template like this:
template<typename T> T max(T &a, T &b) { return a > b ? a : b; }
Structure is entirely irrelevant. All that matters is that T have `>` or the spaceship defined for it.C++ templates get incredibly complex, but at no point is the type system structural. You can add a series of checks which amount to structural typing but that isn't the same thing at all.
Consider the following in a hypothetical language that bears a striking similarity to Typescript. I will leave you to decide if that was on purpose or if it is merely coincidental.
interface Comparable { isGreaterThan(other: Comparable): boolean }
function max(a: Comparable, b: Comparable) { return a.isGreaterThan(b) ? a : b }
As usually defined, if this language accepts any type with an isGreaterThan method as a Comparable, it would be considered an example of structural typing. Types are evaluated based on their shape, not their name. This is the canonical example of structural typing! Ignore the type definitions and you get the canonical example of duck typing!!Now, what if we rewrite that as this?
interface Comparable { >(other: Comparable): boolean }
function max(a: Comparable, b: Comparable) { return a > b ? a : b; }
Staring to look familiar? But let's go further. What if the interface could be automatically inferred by the type checker? function max<interface Comparable>(a: Comparable, b: Comparable) { return a > b ? a : b; }
That is looking an awful lot like: template<typename T> T max(T &a, T &b) { return a > b ? a : b; }
Of course, there is a problem here. Under use, the following will fail in the C++ version, even though both inputs "quack like a duck". The same code, any syntax differences aside, will work in our hypothetical language with structural typing. int x = 1;
float y = 1.0;
max(x, y);
So, yes, you're quite right that your example is not a display of structural typing. But it is also not a display of duck typing (of some compile time variety or otherwise) either. Here, "quacking" is not enough to satisfy the constraints. In order to satisfy the constraints the types need to have the same name, which violates the entire idea behind duck typing.Which is all to say: Your confusion stems from starting with a false premise.
Anyway, your confusion may be easily resolved by perusing the following link.
https://en.wikipedia.org/wiki/Duck_typing#Templates_or_gener...
Say you took a C++ virtual class, split the vtable from the object representation, put the vtable in .rodata, passed its address along with the object. Then write that down explicitly instead of the compiler generating it. Or put the pointer to it in the object if you must. Aside from &mod or similar as an extra parameter, codegen is much the same.
If you heed the "const" word above the compiler inlines through that just fine. There's no syntactic support unless you bring your own of course but the semantics are exactly what you'd want them to be.
Minimal example of a stack of uint64_t where you want to override the memory allocator (malloc, mmap, sbrk etc) and decided to use a void* to store the struct itself for reasons I don't remember. It's the smallest example I've got lying around. I appreciate that "generic stack" usually means over different types with a fixed memory allocator so imagine there's a size_t (*const element_width)() pointer in there, qsort style, if that's what you want.
struct stack_module_ty
{
void *(*const create)(void);
void (*const destroy)(void *);
size_t (*const size)(void *);
size_t (*const capacity)(void *);
void *(*const reserve)(void *, size_t);
// This push does not allocate
void (*const push)(void *, uint64_t);
// pop ~= peek then drop
uint64_t (*const peek)(void *);
void (*const drop)(void *);
};
// functions can call the mod->functions or
// be composed out of other ones, e.g. push can be
// mod->reserve followed by mod->push
static inline uint64_t stack_pop(stack_module mod, void *s) {
uint64_t res = stack_peek(mod, s);
stack_drop(mod, s);
return res;
}
I like design by contract so the generic functions tend to look more like the following in practice. That has the nice trick of writing down the semantics that the vtable is supposed to be providing which tends to catch errors when implementing a new vtable. static inline uint64_t stack_peek(stack_module mod, void *s) {
stack_require(mod);
stack_require(s);
#if STACK_CONTRACT()
size_t size_before = stack_size(mod, s);
size_t capacity_before = stack_capacity(mod, s);
stack_require(size_before > 0);
stack_require(capacity_before >= size_before);
#endif
uint64_t res = mod->peek(s);
#if STACK_CONTRACT()
size_t size_after = stack_size(mod, s);
size_t capacity_after = stack_capacity(mod, s);
stack_require(size_before == size_after);
stack_require(capacity_before == capacity_after);
#endif
return res;
}
> If you heed the "const" word above the compiler inlines through that just fine.
But only when the function it self is inlined, which you quite often don't want. If you sort integers in a bunch of places, you don't really want qsort to be inlined all over the place, but rather that the compiler creates a single specialized copy of qsort just for integers.
With something as simple as qsort compilers sometimes do the function specialization, but it's very brittle and you can't rely on it. If it's not specialized nor inlines then performamce is often horible.
IMO an additional way to force the compiler to specialize a function based on constant arguments is needed. Something like specifying arguments as "inline".
IIRC, this library uses this style of generic programming with a very nice API: https://github.com/JacksonAllan/CC It's just unfortunate that everything needs to get inlined for good performance.
void malloc_stack_drop(void *s) {
stack_drop(&malloc_stack_global, s);
}
If you inline stack_drop into that and user code has calls into malloc_stack_drop, you get the instantiation model back.Absolutely agreed that this is working around a lack of compiler hook. The interface I want for that is an attribute on a parameter which forces the compiler to specialise with respect to that parameter when it's a compile time constant, apply that attribute to the vtable argument. The really gnarly problem in function specialisation is the cost metric, the actual implementation is fine - so have the programmer mark functions as a good idea while trying to work out the heuristic. Going to add that to my todo list, had forgotten about it.
You can already write generic code that way currently, see qsort, but the performance is often very bad, because compilers don't specialize the functions aggresively enoigh.
On the simple level, this would make thigs like qsort always specialize on the comparator and copy operation. But you can use this concept to create quite high level APIs, by passing arround constexpr type descriptor structs that contain functions and parameters operating on the types, somewhat similar to interfaces.
void qsort_i8 ( i8 *ptr, int num);
void qsort_i16(i16 *ptr, int num);
void qsort_i32(i32 *ptr, int num);
void qsort_i64(i64 *ptr, int num);
#if __has_builtin(__builtin_overridable)
__builtin_overridable(qsort, qsort_i8, qsort_i16); // qsort resolves to qsort_i8 qsort_i16
// qsort() is forward compatible and upgradable with your own types
__builtin_overridable(qsort, qsort_i32, qsort_i64); // qsort resolves to qsort_i8 qsort_i16 qsort_i32 qsort_i64
#endif
i8 *ptr0; int num0;
i16 *ptr1; int num1;
i32 *ptr2; int num2;
i64 *ptr3; int num3;
qsort(ptr0, num0); // call qsort_i8()
qsort(ptr1, num1); // call qsort_i16()
qsort(ptr2, num2); // call qsort_i32()
qsort(ptr3, num3); // call qsort_i64()
#include <math.h>
#include <stdio.h>
// Possible implementation of the tgmath.h macro cbrt
#define cbrt(X) _Generic((X), \
long double: cbrtl, \
default: cbrt, \
float: cbrtf \
)(X)
int main(void)
{
double x = 8.0;
const float y = 3.375;
printf("cbrt(8.0) = %f\n", cbrt(x)); // selects the default cbrt
printf("cbrtf(3.375) = %f\n", cbrt(y)); // converts const float to float,
// then selects cbrtf
}
I see the non-proposal and WG14's <Forward Declaration of Parameters> parts of the pursuit to finally standardize passing size info over function calls, with past arts even from Dennis Ritchie himself. While I don't have a strong preference on how it should be done (other than "just use fat pointers already"), I feel some of these too much new syntax for too little improvement. C codebases have established routines for generic types if those were needed, there would be little incentive to adopt a new one. And judging from what Linux[0] and Apple[1] have been doing, if projects are doing annotations at all, they would want a superset of just size or alignment.
[0] https://lpc.events/event/17/contributions/1617/attachments/1... [1] https://llvm.org/devmtg/2023-05/slides/TechnicalTalks-May11/...