fuhsnn a day 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/...

kazinator 3 hours ago

C has generic selection via _Generic. You just have to write your generic function as a macro which uses that in its expansion. It has limitations. It's powerful enough though that for example in the current draft C, I believe, functions like strchr are now overloaded such that if the argument is const char *, you get the const-qualified return type, otherwise plain. There's a header of type generic math as well.

t43562 2 days 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.

  • bsder a day ago

    At this point, I think I'd rather that C simply freeze in amber to open the field to its replacement.

    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.

    • eqvinox 17 hours ago

      It's already frozen and the replacement is already there. It's Rust. (Yes there are other competitors, but unless a large shift happens, Rust has won¹´⁴.)

      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.

      • 1oooqooq 8 hours ago

        rust is a cpp replacement for people who know cpp enough to hate it.

        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.

        • SubjectToChange 3 hours ago

          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.

      • bsder 15 hours ago

        > It's already frozen and the replacement is already there. It's Rust. (Yes there are other competitors, but unless a large shift happens, Rust has won¹´⁴.)

        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.

        • SubjectToChange 3 hours ago

          Rust ain't gonna displace C any time soon. For C++, you might have a better argument.

          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.

          • bsder an hour ago

            > Incrementally replacing C code with Rust is far, far easier than C++.

            And yet other languages manage to do that far better than Rust.

            Interfacing to C from Zig is both pleasant and straightforward, for example.

        • eqvinox 11 hours ago

          Rust is suitable to replace C in some (nontrivial) percentage of cases right now, and that percentage will likely/hopefully increase over time. I agree integrating with existing C build systems is a sore point (in fact my largest personal grievance against Rust — tied to it's very annoying 'modern' habit of "everything brings its own package manager"). But for the time being I don't see any signs of design decisions that would permanently hamper Rust.

          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.

  • wolletd 2 days 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?

    • dundarious a day ago

      There are pretty fundamental differences in key areas, like zero initialization of a struct or array (`{0}` vs `{}`). Use the C way in C++ and you only 0 the first element or member. Use the C++ way in C and you don't 0 anything.

      IMO there's no point even attempting a blind recompile of C as C++. A transpiler tool would be needed.

      • plorkyeran a day ago

        `int array[100] = {0};` results in an array with 100 zeroes in both C and C++. In C if there are too few values the remaining ones are initialized following the same rules as for static variables, and in C++ they're initialized using value initialization. For all types supported by C these have the same result.

    • eqvinox 17 hours ago

      > I mean, most of C just compiles as C++

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

    • jay-barronville a day 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 2 days ago

      > most of C just compiles as C++

      um, not unless explicitly written to so.

    • Gibbon1 a day ago

      We could fire WG14 and add Walter Brights fix for passing arrays, buffer and slice types. Add phat pointers and types as fist class objects.

      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.

    • bsder a day ago

      The ABI for C++ is a disaster. That alone would doom such a conversion.

strlenf a day 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 a day 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 a day 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.

xiphias2 2 days 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.

ww520 2 days 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?

  • SubjectToChange 3 hours ago

    The problem with ISO C standardization is dealing with absolutely impoverished implementations. Said implementations either object to anything moderately complex or can't be trusted to do it right.

cherryteastain 2 days 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 a day 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 a day 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 a day 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.

        • foldr a day ago

          This is just inlining of a function/method parameter. It only has anything at all to do with monomorphization in Rust because lambdas have their own special unwritable types. A good modern C compiler will be able to inline calls to function pointer arguments without doing monomorphization (example here: https://news.ycombinator.com/item?id=31496445). I doubt the Go compiler does such aggressive optimization, but there is nothing to stop it in principle.

          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

          • tialaramex a day ago

            In C++ the lambda function also has an unnameable type, indeed unlike Rust that's a unique property, all of Rust's functions have such types whereas in C++ the named functions have a type based on their signature.

            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 ?

            • foldr a day ago

              I linked an example here in an edit to my original post: https://news.ycombinator.com/item?id=31496445

              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.

              • tialaramex a day ago

                The example seems to just be a boring case where the compiler can compute the answer for your entire program, which isn't interesting at all. Maybe you linked the wrong example?

                • foldr a day ago

                  It can compute the answer because it’s able to inline the function call made via the function pointer argument to 'find'. But if you don’t like that example for some reason, look at the Go example I linked above.

                  • tialaramex a day ago

                    The Go example seems like we're giving the game away much the same as the C. Sure enough if I factor out the predicate, the compiler just... doesn't inline it any more. "Hopefully the optimiser will spot what I meant and optimise it". OK.

                    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.

                    • foldr a day ago

                      Monomorphization does not guarantee inlining of lambdas. You can perfectly well output a monomorphized version of the 'contains' method specialized to the type of a particular lambda argument and then call this lambda on each loop iteration in the generated code. (Rust's assembly output is pretty impenetrable compared to Go's, but I believe this is exactly what happens if you compile something like this without optimizations: https://godbolt.org/z/qaYbGMMzM)

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

      • jchw a day 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.

      • foldr a day ago

        You can use value types rather than pointers/interfaces if that's what you want.

      • neonsunset a day ago

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

  • randomdata a day ago

    > compile time duck typing

    The term you are looking for is structural typing.

    • samatman a day ago

      It isn't though. C++ templates are duck-typed. Structural typing is two structs with the same structure are the same type. Duck typing is when you check if it has a quack field.

      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.

      • randomdata 15 hours ago

        By common definition, structural typing and duck typing end up being the exact same thing, apart from where they are evaluated. With duck typing obviously being evaluated dynamically, and structural typing being evaluated statically. Therefore, "compile time duck typing" is more commonly known as structural typing.

        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.

        • samatman 2 hours ago

          The rhetorical slight of hand you're engaging in here is simple: you're starting with a language in which 1 and 1.0 quack, and then swapping in a language in which 1 and 1.0 do not quack, and hoping, perhaps, that I won't notice?

          Anyway, your confusion may be easily resolved by perusing the following link.

          https://en.wikipedia.org/wiki/Duck_typing#Templates_or_gener...

  • diarrhea a day 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 a day 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 a day 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 a day 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 a day 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 a day 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.

          • randomdata a day ago

            Depending on which implementation you use, the following may produce the same instructions as the example above. Go on, try it!

                 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.
            • cherryteastain a day ago

              Tried it out in godbolt, yes you are right that with the above example gc is able to realize that

                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/zTco4ardx
              • randomdata a day ago

                Was it really necessary to try in gc...? We already talked about how it produces different instructions for the different programs. Nice of you to validate what I already said, I suppose, but this doesn't tell any of us anything we didn't already know.

                The intent was for you to try it in other implementations to see how they optimize the code.

camel-cdr 2 days 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.

rwmj 2 days ago
  • variadix a day ago

    You can get better properties by using the include itself to do the macro expansion, e.g.

    #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

    • eqvinox 17 hours ago

      I agree that the debugging and maintenance experience is much better that way, but it comes at the cost of very poor "DX". We, discussing this here on this thread, understand what's going on. But sadly at least for the projects I'm involved in, the average developer does not.

      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.

  • naasking a day ago

    That monomorphizes every definition, and the point of this proposal was to avoid that.

    • rwmj a day ago

      That's true, although I wonder if a "sufficiently smart compiler" could combine compatible definitions. Also note it's in a header file and most of these functions are minimal enough they would be inlined.

      • eqvinox a day ago

        I'd distinguish here and not label one-liner "cast wrappers" as monomorphization. And then you can in fact do a lot with macros.

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

choeger 2 days 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 2 days ago

    Modern CPUs make following pointers quite slow.

JonChesterfield 2 days 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 a day 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 a day 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.

  • eqvinox 16 hours ago

    The entire point of this discussion is to get the compiler to tell you when you're accidentally mixing up things — which you don't get with void pointers. Your example doesn't exhibit this particularly well, try looking at a generic simple linked list or so.

anacrolix 2 days ago

Just use Zig?

  • pfg_ 2 days 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_ 2 days 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 a day ago

    Just use D?

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

  • samatman a day ago

    And a lot of people will, but we're not taking C out back of the barn and shooting it. It's here to stay and adding some new features which make it a better language is a good idea.

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