Allow Plan 9/Go style struct embedding
isaachier opened this issue Β· 10 comments
GCC allows use of -fplan9-extensions
to accomplish Go-style anonymous struct embedding. See more here. I believe this is a simple C concept (seeing as it was a Plan 9 C compiler extension), so does not conflict with the C-style simplicity of Zig, and adds some nice abstractions to structs for free. The main benefit: extending a struct is by and large unnecessary when the compiler let's you treat the parent struct like it was an instance of the embedded base class (Liskov substitution principle applies to parent structs).
One quick example, stdout
often needs to be replaced with stdout.stream
despite the fact the stdout
is meant to implement a stream. This could simplify all uses of stdout.stream
back to stdout
.
IIRC Zig reserves the right to reorder fields. So the child struct may not have the same field order as the parent.
What's the problem with that @kyle-github?
If the compiler can reorder fields, then your "parent" struct might not be embedded at the beginning of the "child" struct. Or, if you flatten it, the fields in the parent part might be in a different order (or even mixed with) the fields of the child part.
In other words, for this to work sanely, you also must require that fields are not changed in order from the way they are declared. There are two main requirements that C makes: the first is that a pointer to a struct is a pointer to the first field (lexically) in the struct, the second is that fields are never reordered by the compiler, they are always in the exact order given in the struct definition.
Since things as remote as cache line size can impact field ordering choices, placing restrictions on field ordering just for this purpose seems like a large cost. C pays for it all the time now.
@kyle-github that would be true for upcasting, where you must assume the base class is at offset 0 of the struct. But the Plan 9 extension is a bit more elegant. Agreed the field reordering would not be true for C, so the function knows the offsets of each field from the base struct alone. However, it can do this:
typedef struct A {
double y;
const char* z;
} A;
typedef struct B {
int x;
A;
} B;
void print_a(const A* a) {
printf("y = %.2f, z = %s\n", y, z);
}
void print_b(const B* b) {
printf("x = %d, ");
print_a(b); /* Compiler automatically uses offset of A within b for this function call. No upcasting. */
}
The GCC example is here:
struct s1 { int a; };
struct s2 { struct s1; };
extern void f1 (struct s1 *);
void f2 (struct s2 *p) { f1 (p); }
Same idea.
@kyle-github, that doesn't stop access to any other field, so why would it stop access for an embedded struct?
In Go, it's not actually inheritance, it's really just sugar. There are a few facets to this:
-
The containing type may have fields and methods from the embedded struct referred to directly, unless their names collide with names in the container. Those must be referred to explicitly on the embedded struct.
-
The embedded struct is not really anonymous. You can access it as a field by referring to it by the type name (such as rw.Reader). So that solves the "problem" of rearranging fields -- the embedded struct itself is just a named field.
-
The containing type may be used where an interface is wanted if names required by the interface can be resolved by referring to names from the contained struct.
-
The containing type may be used where a concrete type is required if the embedded type is that type. In this usage, only the embedded type is passed. This is not a type conversion or inheritance behavior. It is just passing the embedded struct by referring to the field on the containing type.
-
In particular, this means that there is NOT an OO class relationship here. You cannot "upcast" the container. There is no "conversion" happening at all. The embedded struct does not have any intermixture with the containing type's fields or names, etc.
Hope this info is helpful to clarify OP's intent. I favor this idea because it's quite convenient in Go, and also plays nice with Go's design for interfaces.
Thanks @binary132 for explaining. Ya the Plan 9 compiler extensions do exactly the same thing as Go does. Even referring to the member by the anonymous member's type works (note the GCC example I provided does this).
Note: this feature tends to be really great. Being able to call foo.lock() on an arbitrary struct foo is awesome. This code is from portdat.h of the Plan 9 kernel.
struct Rendez
{
Lock;
Proc *p;
};
struct QLock
{
Lock use; /* to access Qlock structure */
Proc *head; /* next process waiting for object */
Proc *tail; /* last process waiting for object */
int locked; /* flag */
};
struct Rendezq
{
QLock;
Rendez;
};
Example of an ugly-ness this issue will solve: https://github.com/ziglang/zig/pull/2525/files#diff-41648b87f70e3cdb2d95dc1ce9ed9f38R1042
a possible use case this would improve would be API loaders. @MasterQ32's and my own OpenGL loaders would be able to cut down on a lot of code repetition if we could "inherit" fields from other structs.
After discussing at the design meeting, we've decided that this is too complicated. If we did this, we would need additional complexity to handle cases like #7391, where the outer struct needs to preserve extra invariants in the inner one. In Go, you can override the implementation of an embedded member function when called through the outer struct by providing another with the same name and signature. We feel that this ability to override functions would be necessary to make this form of embedding usable, but is also too complicated to include in this language.
Additionally, all of these use cases can be solved with the current language, and the syntax to do them isnβt all that verbose. a.b.foo()
is more explicit than a.foo()
, and not too bad to type. Or you can write a wrapper function in a
that forwards the call to .b
.
The reduced form described in #5561 is much simpler, but also much less useful, and possible with the current language semantics, so that has also been rejected. We haven't reached a decision on the more fine grained #7311. We have accepted a variant of #985, which solves the specific use case of interfacing with C libraries like io_uring that use unnamed fields of anonymous struct/unions.