[HN Gopher] Type-erased generic functions for C: A modest non-pr...
       ___________________________________________________________________
        
       Type-erased generic functions for C: A modest non-proposal
        
       Author : thunderbong
       Score  : 77 points
       Date   : 2024-09-30 14:34 UTC (3 days ago)
        
 (HTM) web link (duriansoftware.com)
 (TXT) w3m dump (duriansoftware.com)
        
       | rwmj wrote:
       | Meh, you just need more macros ...
       | 
       | https://gitlab.com/nbdkit/nbdkit/-/blob/296b9dd041fdbcbaa731...
        
         | eqvinox wrote:
         | <300 lines? Peanuts! ;D
         | 
         | https://github.com/FRRouting/frr/blob/master/lib/typesafe.h
         | 
         | https://docs.frrouting.org/projects/dev-guide/en/latest/list...
        
         | naasking wrote:
         | That monomorphizes every definition, and the point of this
         | proposal was to avoid that.
        
           | rwmj wrote:
           | 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 wrote:
             | 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 offsets1 on arguments/retval are
             | allowed, as well as adding sizeof/offsetof/constant integer
             | arguments, but nothing else.)
             | 
             | 1 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.
        
         | variadix wrote:
         | 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
        
           | david2ndaccount wrote:
           | I wrote a blogpost describing this technique.
           | https://www.davidpriver.com/ctemplates.html
        
       | choeger wrote:
       | 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 wrote:
         | Modern CPUs make following pointers quite slow.
        
       | anacrolix wrote:
       | Just use Zig?
        
         | pfg_ wrote:
         | 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_ wrote:
           | 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 wrote:
         | Just use D?
         | 
         | The point is not to use something else, but to improve what
         | they already use
        
         | samatman wrote:
         | 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...
        
       | ww520 wrote:
       | Zig's comptime has wonderful support for generic. It's very
       | powerful and intuitive to use. May be can borrow some ideas from
       | it?
        
       | t43562 wrote:
       | 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 wrote:
         | 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?
        
           | zabzonk wrote:
           | > most of C just compiles as C++
           | 
           | um, not unless explicitly written to so.
        
           | jay-barronville wrote:
           | > 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
        
           | dundarious wrote:
           | 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 wrote:
             | `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.
        
           | Gibbon1 wrote:
           | 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 wrote:
           | The ABI for C++ is a disaster. That alone would doom such a
           | conversion.
        
         | bsder wrote:
         | 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.
        
       | xiphias2 wrote:
       | 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 wrote:
       | 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 wrote:
         | > 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 wrote:
           | 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 wrote:
             | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | 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.
        
             | neonsunset wrote:
             | Yup, much like Rust and C# (with structs).
        
             | jchw wrote:
             | 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 wrote:
             | You can use value types rather than pointers/interfaces if
             | that's what you want.
        
         | diarrhea wrote:
         | > 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 wrote:
           | 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 wrote:
             | 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 wrote:
               | 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 wrote:
             | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | 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.
        
         | randomdata wrote:
         | _> compile time duck typing_
         | 
         | The term you are looking for is structural typing.
        
           | samatman wrote:
           | 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.
        
       | JonChesterfield wrote:
       | 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 wrote:
         | > 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 wrote:
           | 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.
        
       | camel-cdr wrote:
       | 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.
        
       | strlenf wrote:
       | 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 wrote:
         | 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 wrote:
           | 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.
        
       | fuhsnn wrote:
       | 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/...
        
       ___________________________________________________________________
       (page generated 2024-10-03 23:01 UTC)