In Zig or any other C like language without "interfaces", you would implement the V-table by hand, which is a common idiom in Zig.
In Zig or any other C like language without "interfaces", you would implement the V-table by hand, which is a common idiom in Zig.
I'm a bit confused about when you would construct this table and how one would use it
typedef struct Writer {
int (*method_one)(const char*, int);
//...
} Writer;
void takes_a_writer(const Writer *obj) {
// somewhere
obj->method_one(buff, buff_size);
// ...
}
// a method implementation
int method_one_impl(const char *buff, int n) {
//...
}
// somewhere else
Writer w;
w.method_one = method_one_impl;
//...
takes_a_writer(&w);
It isn't the best example, but should do the job giving you an overview.In order to make this mechanism generic, you can instead say, "every implementer of the 'Reader' interface has a pointer as its first field, and that pointer leads to an array of two elements: the first element is a pointer to a 'Read' method, the second to a 'Close' method."
This way, the user of a Reader knows nothing of the internals of each implementation, other than how to find its methods
const std = @import("std");
// base struct
const Animal = struct {
// points to the derived struct
ctx: *anyopaque,
// points to the vtable of the concrete type
vtable: *const VTable,
// the vtable interface derived struct must implement
const VTable = struct {
make_noise: *const fn (ctx: *anyopaque, loudness: u32) anyerror!void,
};
// call the derived struct's implementation
pub fn make_noise(animal: Animal, loudness: u32) anyerror!void {
return animal.vtable.make_noise(animal.ctx, loudness);
}
};
const Dog = struct {
// extra data
weight: u32,
// implement the interface
const vtable = Animal.VTable{
.make_noise = &make_noise,
};
pub fn make_noise(ctx: *anyopaque, loudness: u32) anyerror!void {
const dog: *Dog = @alignCast(@ptrCast(ctx));
std.debug.print("woof {} {}\n", .{ dog.weight, loudness });
}
// helper to convert to the base struct
pub fn _animal(self: *Dog) Animal {
return Animal{
.ctx = @ptrCast(self),
.vtable = &vtable,
};
}
};
const Cat = struct {
weight: u32,
const vtable = Animal.VTable{
.make_noise = &make_noise,
};
pub fn _animal(self: *Cat) Animal {
return Animal{
.ctx = @ptrCast(self),
.vtable = &vtable,
};
}
pub fn make_noise(ctx: *anyopaque, loudness: u32) anyerror!void {
const cat: *Cat = @alignCast(@ptrCast(ctx));
std.debug.print("meow {} {}\n", .{ cat.weight, loudness });
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
// list of base structs
var animal_list = std.ArrayList(Animal).init(alloc);
defer {
for (animal_list.items) |animal| {
if (animal.vtable == &Dog.vtable) {
const dog: *Dog = @alignCast(@ptrCast(animal.ctx));
alloc.destroy(dog);
} else if (animal.vtable == &Cat.vtable) {
const cat: *Cat = @alignCast(@ptrCast(animal.ctx));
alloc.destroy(cat);
}
}
animal_list.deinit();
}
for (0..20) |i| {
if (i % 2 == 0) {
var dog = try alloc.create(Dog);
dog.* = Dog{ .weight = @intCast(i) };
try animal_list.append(dog._animal());
} else {
var cat = try alloc.create(Cat);
cat.* = Cat{ .weight = @intCast(i) };
try animal_list.append(cat._animal());
}
}
// meows and woofs here
for (animal_list.items) |animal| {
try animal.make_noise(10);
}
}
ive written a couple and still find them mindbendy const Animal = union(enum) {
cat: Cat,
dog: Dog,
pub fn make_noise(self: Animal) void {
switch (self) {
inline else => |impl| impl.make_noise(),
}
}
};The rest of OOP is lipstick on arrays and arrays of arrays and "structs / records" or software defined arrays.
In my opinion.