• fuhsnn 4 hours ago

    This is too close to C++ templated functions, I would prefer just use C++ syntax with stricter rules like C23 did with constexpr and auto. Being able to interop/share code with C++ with extern "C"{ template<T> } wouldn't hurt either.

    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/...

    • t43562 7 hours ago

      For new code you can use any language but we do have a lot of C code out there so it would be nice to have some ways to modernize it in, say 10 years, when those modernizations have made it out to every conceivable platform that runs the software.

      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.

      • wolletd 6 hours ago

        I wonder how hard it would be to just convert old C code to C++?

        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?

        • jay-barronville 4 hours ago

          > I mean, most of C just compiles as C++ and does the right thing.

          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.

          [0]: https://github.com/microsoft/mimalloc

          • zabzonk 6 hours ago

            > most of C just compiles as C++

            um, not unless explicitly written to so.

        • xiphias2 7 hours ago

          It's a cool idea, simple, I like it much more than the _Var and _Type stuff, but I'm not sure if it's 100% backwards compatible:

          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.

          • cherryteastain 5 hours ago

            Looks very similar to Go generics, which also don't support monomorphization. Sadly, Go's generics are a bit of a basket case in terms of usefulness since they don't support compile time duck typing like C++ generics, but I imagine it won't be a problem for C since it does not have methods etc like Go. That said, I personally feel like that's precisely why I feel like this proposal adds little value compared to macros, since runtime dispatch is bad for performance (very important for most uses of C) and the convenience added is small because C only supports a few primitive types plus POD structs without embedding/inheritance.

            • mseepgood 5 hours ago

              > Looks very similar to Go generics, which also don't support monomorphization.

              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...

              • cherryteastain 4 hours ago

                Go does monomorphization in a rather useless manner from a performance perspective because all pointers/interfaces are essentially the same "shape". Hence you still have no inlining and you do not avoid the virtual function when any interfaces are involved as detailed in https://planetscale.com/blog/generics-can-make-your-go-code-...

                C++ fully monomorphizes class/function templates, and therefore incurs 0 overhead while unlocking a ton of inlining, unlike Go.

                • tialaramex 3 hours ago

                  Yup. The most classic example of this is that in both Rust and C++ we can provide a method on some type which takes a callable and instead of a single implementation which runs off a function pointer, the compiler will separately monomorphize individual uses with lambdas to emit the optimised machine code for the specific case you wrote often inline.

                  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.

                  • jchw 2 hours ago

                    Go developers didn't really want C++ templates. C++ templates are very powerful, but while they unlock a lot of runtime performance opportunities, they are also dog-slow for compilation.

                    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.

                    • neonsunset 3 hours ago

                      Yup, much like Rust and C# (with structs).

                  • diarrhea 5 hours ago

                    > Sadly, Go's generics are a bit of a basket case in terms of usefulness since they don't support compile time duck typing like C++ generics,

                    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.
                    • cherryteastain 4 hours ago

                      The example you gave is the most trivial one possible. There is 0 reason to write that code over PrintAnything(item Stringer). Go doesn't even let you do the following:

                        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++.

                      • diarrhea 2 hours ago

                        Interesting, thank you for the example. I'm mostly used to how Rust handles this, and in its approach individual items such as functions need to be "standalone sane".

                            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.
                        • tialaramex an hour ago

                          Lots of C++ is driven by textual substitution, the same mess which drives C macros. So, not magic, but the resulting compiler diagnostics are famously terrible since a compiler has no idea why the substitution didn't work unless the person writing the failed substitution put a lot of work in to help a compiler understand where the problem is.

                        • assbuttbuttass an hour ago

                          No, the equivalent go code would be

                            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))
                            }
                          • cherryteastain an hour ago

                            It is not equivalent because, per the monomorphization discussion above, putting an interface in there means that you incur the cost of a virtual function call. The C++ code will compile down to simply accessing a struct member once inlined while the Go code you wrote will emit a ton more instructions due to the interface overhead.

                    • strlenf 5 hours ago

                      Here is my not-very-novel proposal for function overriding only. Its backward/forward compatible and free of name-mangling.

                        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()
                      • Someone 4 hours ago

                        You can do that since C11 using _Generic. https://en.cppreference.com/w/c/language/generic:

                          #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
                          }
                        • strlenf 3 hours ago

                          True but its very clumsy for large number of parameters. Even more importantly, its not forward compatible. For example, cbrt() cant reliably expanded to support bigints or any private types.

                      • camel-cdr 5 hours ago

                        There is a very simple and small adition to C, that would solve the generics problem, albeit without much typesafety and you'd probably need a bunch of macros to create really nice APIs: Marking function arguments as "inline" such that the argument must always be a constant expression and the compiler is supposed/forced to specialize the function.

                        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.

                        • ww520 7 hours ago

                          Zig’s comptime has wonderful support for generic. It’s very powerful and intuitive to use. May be can borrow some ideas from it?

                          • rwmj 8 hours ago
                          • choeger 8 hours ago

                            I really don't see what the problem with the runtime metadata is? In the example, the size of T is also an argument? So why not pass a pointer to the whole set of type information there?

                            • rwmj 6 hours ago

                              Modern CPUs make following pointers quite slow.

                            • JonChesterfield 5 hours ago

                              C is, of course, totally up for writing generic functions already. The patten I like most is a struct of const function pointers, instantiated as a const global, then passed by address to whatever wants to act on that table.

                              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;
                                  }
                              • camel-cdr 5 hours ago

                                > The patten I like most is a struct of const function pointers, instantiated as a const global, then passed by address to whatever wants to act on that table

                                > 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.

                                • JonChesterfield 5 hours ago

                                  Sort of. If you want the instantiation model, you can go with forwarding functions (maybe preprocessor instantiated):

                                      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.

                              • anacrolix 7 hours ago

                                Just use Zig?

                                • pfg_ 7 hours ago

                                  This proposal is different than how zig handles generic functions because it only emits one symbol and function body for each function.

                                  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.

                                  • pfg_ 7 hours ago

                                        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 C
                                  • WhereIsTheTruth 5 hours ago

                                    Just use D?

                                    The point is not to use something else, but to improve what they already use