Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

For your level 2 code, `uint64_t data[];` is wrong for types whose alignment is greater than `uint64_t`, and also wasteful for types whose alignment is smaller (for example, under an ilp32 ABI on 64-bit architectures).

For your level 3 code, it should be `int main() { List(Foo) foo_list = {NULL};`

Note that working around a lack of `typeof` means you can't return anything. Also, your particular workaround allows `const`ness errors since `==` is symmetrical.

You can't safely omit `payload` since you need it to know the correct size. Consider a `List(int64_t)` and you try to add an `int32_t` to it - this should be fine, but you can't `sizeof` the `int32_t`. Your code is actually lacking quite a bit to make this work.

=====

There are 2 major limitations to generics in C right now:

* Delegating to a vtable (internal or external) is limited in functionality, since structs cannot contain macros, only functions.

* Delegating to an external vtable (mandatory to avoid overhead) means that you have to forward-declare all of the types you'll ever use a vtable with. So far the best approach I've found is to declare (but not define) static functions in the same forwarding header I declare the typedefs in; note that GCC and Clang differ in what phase the "undefined static" warning appears in for the case where you don't actually include that particular type's header in a given TU.

(think about writing a function that accepts either `struct SizedBuffer {void *p; size_t len;};` or `struct BoundedBuffer {void *begin; void *end;};`, and also const versions thereof - all from different headers).



> Delegating to an external vtable (mandatory to avoid overhead) means that you have to forward-declare all of the types you'll ever use a vtable with.

We went down the rabbit hole of writing a compiler for this as part of a project I used to work on (Apache Clownfish[1], a subproject of the retired Apache Lucy project). We started off parsing .h files, but eventually it made sense to create our own small header language (.cfh "Clownfish Header" files).

Here's some generated code for invoking the CharBuf version of the "Clone" method defined in parent class "Obj":

    typedef cfish_CharBuf*
    (*CFISH_CharBuf_Clone_t)(cfish_CharBuf* self);

    extern uint32_t CFISH_CharBuf_Clone_OFFSET;

    static inline cfish_CharBuf*
    CFISH_CharBuf_Clone(cfish_CharBuf* self) {
        const CFISH_CharBuf_Clone_t method
            = (CFISH_CharBuf_Clone_t)cfish_obj_method(
                self,
                CFISH_CharBuf_Clone_OFFSET
            );
        return method(self);
    }
Usage:

    cfish_CharBuf *charbuf = cfish_CharBuf_new();
    cfish_CharBuf *clone = CFISH_CharBuf_Clone(charbuf);
We had our reasons for going to these extremes: the point of Clownfish was to provide a least-common-denominator object model for bindings to multiple dynamic languages (similar problem domain to SWIG), and the .cfh files also were used to derive types for the binding languages. But there was truly an absurd amount of boilerplate being generated to get around the issue you identify.

This is why almost everybody just uses casts to void* for the invocant, skipping type safety.

[1] https://github.com/apache/lucy-clownfish


i am firmly of the opinion that compiling to c is a better route than doing clever c tricks to sort of get what you want. the compiler can be pretty minimal and as you note it pays for itself.


There’s some prior work called CFront. It implements a superset of C that’s just an “increment”. I think it’s worth looking into, it might take off one day!


Here I've got it to work with recent compilers/OSs https://github.com/mingodad/cfront-3


I dunno, I looked at that a while back - the version for MS-DOS shipped on multiple floppy disks. Not exactly lightweight!


> it should be `int main() { List(Foo) foo_list = {NULL};`

In C `int main()` means the function takes an unknown number of arguments. You need `int main(void)` to mean it doesn't take any arguments. This is a fact frequently forgotten by those who write C++.


That had been harmonized with C++ in C23 (e.g. func() is equivalent with func(void) now).

It's not really relevant for main() though, even in older C versions main() works fine and simply means "I don't need argc and argv".


This is about a function definition, not a random function declarator. C23 does not change anything in that case.


This is incorrect. In a function definition, an empty list means it takes no parameters. 6.7.5.3 Function declarators

> 14. An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters.


"has no parameters" is not the same as "cannot take arguments". Defining `int main()` does not stop the runtime from passing the usual 3 arguments (typically named argc, argv, envp), it only means that no parameters are bound to those arguments. Technically it's no problem to have a C function ignore its arguments by not binding parameters. Way too many programmers seem to not understand the difference between parameter and argument.

https://stackoverflow.com/questions/156767/whats-the-differe...


> "has no parameters" is not the same as "cannot take arguments".

In C I guess that's true. In languages more concerned with compile-time rigor, it often isn't. Not a correction, just an observation.


Surely part of the problem is having a distinct term and handling for parameters passed to functions. What is the point? It seems confusing with no upside.


Do you find the difference between abstract and concrete confusing? Or the difference between container and contents? Is that a pointless distinction with no upside?


I do agree these are useful concepts to distinguish, but I don't get the connection to the topic at-hand. To me, there is just the function signature. I don't see a benefit to referring to passed values as distinct from received values. To my ear "argument" and "parameter" are perfect synonyms.


> referring to passed values as distinct from received values.

That’s not the distinction being made by those terms.

“Parameter” refers to a named variable in a function definition.

“Argument” refers to an actual value that’s passed to a function when it’s called.

It’s exactly the same as the distinction between variables and values (which you probably see the use for), just applied to the special cases of function signatures and function calls.

(As an aside, in the lambda calculus this relationship becomes a perfect equivalence: all variables are parameters and all values are arguments.)


Well, I certainly would not interpret the terms that way, but you do you.


It's the standard definition in a software development context, which you can find all over the place. Here are some examples:

> "Parameters are named variables declared as part of a function. They are used to reference the arguments passed into the function."

-- MDN, https://developer.mozilla.org/en-US/docs/Glossary/Parameter

> "A parameter is a special kind of variable used in a function to refer to one of the pieces of data provided as input to the function. These pieces of data are the values of the arguments with which the function is going to be called/invoked."

-- Programming Fundamentals, https://press.rebus.community/programmingfundamentals/chapte...

> "Parameters refer to the variables listed in a function's declaration, defining the input that the function can accept. Arguments, however, are the actual values passed to the function when it is called, filling the parameters during execution."

-- https://www.geeksforgeeks.org/computer-science-fundamentals/...

While you might be tempted to "do you" and use your own idiosyncratic definitions, I advise against it, since it makes it difficult for you to understand what others are saying, and vice versa.


There is nothing to "interpret". In programming, those terms are defined that way.


lol, it's not a "you do you" thing, that's what they're actually named, "parameters" and "arguments" have distinct objective definitions in this context and those are it. In this specific case it's you who's using made up words for concepts that others already have a specific name for.


> that's what they're actually named

...by what authority? c'mon, communication is important, and insisting on the correctness of definitions tanks that.

EDIT: however, I will concede there's good evidence for widespread usage of this, and I'll adjust my usage accordingly. Insisting on "correctness" is just asinine, though.


As you surely know if you're quoting the standard, it depends on which standard!


Quote a different standard.


I believe that since C23 foo() is now a nullary function. As this is the last approved standard and it supersedes all previous standards, it is technically correct to say that de-jure this is what the (unqualified) C standard mandates.

Of course de-facto things are more nunanced.


C23 does not change anything in this situation, because we are talking about the definition of main(), not a forward declaration. More details here:

https://news.ycombinator.com/item?id=38729278#38732366


In what situation fn() doesn't mean fn(void) under C23?


None, but that is not my point. Before C23, fn() already meant the same thing as fn(void) in function definitions, which the situation under discussion here.

C23 changed what fn() means outside a function definition.


Oh, yeah, the codegen for the fn() itself would likely be the same, but the prototype of that definition is still a K&R function. https://godbolt.org/z/Psvae55Pr


I would love for `union`s to be federated, that is, a type could declare itself as thought it was part of a union with another type, without having to pre-declare all possible types in one place.


Can't you just declare anonymous unions whenever? E.g.

    struct foo { ... };
    
    struct bar { ... };

    struct bar check_this_out(struct foo foo) {
        return ((union { struct foo foo; struct bar bar; }){ .foo = foo}).bar;
    }

    struct bar *also_this(struct foo *foo) {
        union { struct foo foo; struct bar bar; } *tmp = (void*)foo;
        return &tmp->bar;
    }


For layout-compatible types, you can often just include a `_base` member in each child. Maybe twice (once named and once unnamed) to avoid excess typing - I don't understand the common-initial-subsequence rule but people do this enough that compilers have to allow it.


I don't think compilers typically allow that: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=14319


This is also problematic, because there might be padding and the calculated size might be too small:

`malloc(sizeof(*node) + data_size);`


There's no a problem with the author's current code, since the padding is already included in the node size, but it would be a problem after doing alignment more intelligently.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: