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