Structs genéricos

Structs genéricos

 Podemos crear structs genéricos retornándolos desde una función genérica usando la sintaxis siguiente:


fn nombre_func ( T: type) type {

  return struct {

     //…  construimos un struct (tipo nuevo) en función del T

     // con sus métodos y campos

  };

}


Al igual que sucede con las funciones genéricas: el compilador usará el código genérico como plantilla para generar versiones especializadas de struct por cada uso que aparece en el código fuente.

De esta manera obtenemos una gran flexibilidad a la hora de encapsular los datos de diferentes tipos sin duplicar la lógica.

Después del accidentado y ruidosísimo “aterrizaje” de la máquina voladora contra el palo de la hoguera, la muchedumbre de la plaza había huido despavorida. Creerían que se trataba de un dragón o del mismo demonio. Tampoco parecía quedar ningún inquisidor alrededor.

Manitas se acercó al inventor y después de observar un rato su situación en total silencio -como si de una avería de los cazos oscilantes se tratase; Juanelo hasta pensó que se había atascado: “vaya momento para una avería amico”-  por fín puso sus manos a la obra intentando cortar las cuerdas que ataban al inventor al poste. Bueno, más que manos - la mano que todavía funcionaba después del accidente.

- Grazie Manite, menudo trastazzo te has pegado amico.

Manitas no contestó. No tenía ningún tipo de módulo para hablar ni comunicarse. Así, siempre trabajando en silencio, acompañado solo por el suave chirrido de sus propias palancas y engranajes… hasta parecía más sabio, pensó Juanelo.

Todavía no terminaba de terminar de cortar las gruesas cuerdas cuando Juanelo divisó un par de inquisidores volviendo a la plaza espada en mano. Les vieron y, trás dudar un instante, empezaron a acercarse.

-Es el hombre de palo que ha caído del cielo. Está soltando al brujo -escuchó decir a los clérigos armados.

- Manite, amico. Tenemos que irnos - dijo apresurado.

Manitas levantó la cabeza y los vió por fin. Se levantó, giró en dirección contraria y empezó a alejarse caminando despacio.

- Manite, no me dejes aquí amico. ¡No! - gritó Juanelo desesperado.

Ya no eran solo un par. Los inquisidores volvían a la plaza decididos a matarle definitivamente.

- ¡Manite no te vayas! ¡Socorro! ¡Vuelve -gritó Juanelo que desde su posición en el suelo apenas podía ver al autómata con claridad.

Manitas parecía estar averiado por la caída. “Intenta escapar en la máquina voladora”, pensó el inventor al verle meter la mano entre los restos de la nave. Y entonces se dio cuenta…

- ¡En nombre de Dios! -tronaba de nuevo vez el inquisidor, acercándose cada vez más embravecido desde la otro lado de la plaza.

- ¡Brujo! ¡Vas a morir de mi espada y luego quemaremos tus restos!

Manitas sacó la ametralladora del interior de la máquina estrellada.  Como siempre, en silencio, acompañado por el suave zumbido de sus engranajes, encajó un pesado cargador lleno de proyectiles metálicos en el arma. Levantó la mirada y apuntó hacía los inquisidores que venían corriendo… y giró la manivela.

magazines.zig

const std = @import("std");

const print = std.debug.print;

const Metal = enum {

    Iron,

    Steel,

    Copper,

    const N_SIZE: u8 = 1;

};

const Nature = enum {

    Wood,

    Rock,

    const N_SIZE: u8 = 2;

};

// devuelve un cargador para un tipo de proyectil

fn pack_magazine(T: type) type {

    return struct {

        // capacidad depende del tamaño de los proyectiles

        const N_MAG_CAPACITY: u8 = 100 / T.N_SIZE;

        a_slots: [N_MAG_CAPACITY]T = undefined,

        l_ammo: std.ArrayListUnmanaged(T) = undefined,

        b_initialized: bool = false,

        fn init(self: *@This()) void {

            self.l_ammo = std.ArrayListUnmanaged(T)

                .initBuffer(&self.a_slots);

            self.b_initialized = true;

        }

        fn load_ammo(self: *@This(), x_item: T) bool {

            if (!self.b_initialized) self.init();

            if (self.l_ammo.items.len == self.a_slots.len) {

                print("No caben más proyectiles \n", .{});

                return false;

            }

            self.l_ammo.appendAssumeCapacity(x_item);

            return true;

        }

    };

}

// ametralladora que puede usar distintos tipos de cargador

const MachineGun = struct {

    p_magazine: ?*anyopaque = null, // puntero a magazine

    // puntero a función anónima específica para el tipo de cargador

    fn_empty_mag: ?*const fn (*anyopaque) void = null,

    // meter el cargador

    fn load_mag(self: *@This(), x_mag: anytype) void {

        // nos quedamos el tipo del cargador

        const T = @TypeOf(x_mag);

        const o_tinfo = @typeInfo(T);

        // contrato mínimo de un cargador

        if (!@hasField(

            o_tinfo.pointer.child,

            "l_ammo",

        )) return;

        self.p_magazine = x_mag;

        // función anónima que sirve para poder procesar un tipo específico

        self.fn_empty_mag = struct {

            fn _(p_magazine: *anyopaque) void {

                const p_mag: *o_tinfo.pointer.child =

                    @ptrCast(@alignCast(p_magazine));

                if (!p_mag.*.b_initialized or

                    p_mag.*.l_ammo.items.len == 0)

                    return print("click? (sin balas)\n\n", .{});

                while (p_mag.*.l_ammo.items.len > 0)

                    print(

                        "{s}! ",

                        .{@tagName(p_mag.*.l_ammo.orderedRemove(0))},

                    );

                print("\n", .{});

            }

        }._;

    }

    // dispara todos los proyectiles vaciando el cargador

    fn shoot(self: *MachineGun) void {

        // si no hay cargador tampoco apunta a una función de disparo

        if (self.fn_empty_mag) |fn_x|

            fn_x(self.p_magazine.?)

        else

            print("clack? (sin cargador)\n\n", .{});

    }

};

pub fn main() !void {

    var o_gun = MachineGun{};

    // tiro sin cargador puesto

    o_gun.shoot();

    var o_mag1 = pack_magazine(Metal){};

    var o_mag2 = pack_magazine(Nature){};

    print("Llenar los cargadores\n", .{});

    const fill_mag = struct {

        fn _(o_m1: anytype, o_m2: anytype) void {

            for (0..100) |_| {

                if (!o_m1.load_ammo(Metal.Iron) or

                    !o_m1.load_ammo(Metal.Steel) or

                    !o_m2.load_ammo(Nature.Rock))

                    break;

            }

        }

    }._;

    // cargadores con balas

    fill_mag(&o_mag1, &o_mag2);

    // cargador 1 puesto

    o_gun.load_mag(&o_mag1);

    o_gun.shoot();

    o_gun.shoot(); // por error

    // cargador 2 puesto

    o_gun.load_mag(&o_mag2);

    o_gun.shoot();

    // carga de balas

    fill_mag(&o_mag1, &o_mag2);

    // cargador 1 puesto

    o_gun.load_mag(&o_mag1);

    o_gun.shoot();

}

$ zig run magazines.zig

clack? (sin cargador)

Llenar los cargadores

No caben más proyectiles

Iron! Steel!...

En este ejemplo, la ametralladora primitiva de Juanelo fue modificada esa misma mañana para aceptar cargadores de diferentes tipos.

Para obtener un cargador adaptado a un tipo de munición usamos la función pack_magazine, que nos devuelve un struct genérico que lo representa:

  // devuelve un cargador para un tipo de proyectil

  fn pack_magazine(T: type) type {

    return struct {

      // campos y funciones propias

    };

  }

Evitamos repetir la lógica de un cargador (sus métodos y campos), pero seguimos trabajando con un tipado fuerte. Además como ejemplo podemos modificar propiedades como la capacidad dependiendo del tipo de proyectil:

  // capacidad depende del tamaño de los proyectiles

  const N_MAG_CAPACITY: u8 = 100 / T.N_SIZE;

  a_slots: [N_MAG_CAPACITY]T = undefined,

Nuestra ametralladora ahora es totalmente agnóstica al tipo de proyectil que dispara y al tipo de cargador que ponemos. Esta vez, en el cargador caben más o menos balas dependiendo de su tipo, y no pueden ser mezcladas diferentes clases de proyectil (imaginemos que así evitamos atascos).

Anyopaque

El cargador de la ametralladora se “carga” en un puntero llamado p_magazine que declaramos como anyopaque.  Lo hemos marcado con ? para poder iniciarlo como null.

anyopaque quiere decir que es un puntero con el tipo borrado: apunta a memoria sin información de tipo ni tamaño en el momento de su declaración, pero que no está vacío en tiempo de ejecución.

  // ametralladora que puede usar distintos tipos de cargador

  const MachineGun = struct {

    p_magazine: ?*anyopaque = null, // puntero a magazine

    // ...

Para poder procesar (disparar) las balas de ese cargador apuntado por el tipo anyopaque, declaramos una propiedad llamada fn_empty_mag. Esta es un puntero a función y ahora te explico por qué lo necesitamos.

 

    // puntero a función anónima específica para el tipo de cargador

    fn_empty_mag: ?*const fn (*anyopaque) void = null,

Este puntero a función es también marcado como optional para poder inicializarlo a null en su declaración. Declaramos que aceptará como único argumento un puntero a anyopaque que en realidad será el cargador de tipo desconocido apuntado por p_magazine.

Usamos anyopaque porque, para el arma, el tipo de cargador es totalmente desconocido y le da lo mismo disparar perdigones de roca, madera o de acero. Hemos impuesto muy pocos límites en ese sentido.

@This()

Al montar el cargador, con la función load_mag, es cuando la ametralladora obtiene el tipo y su información estructurada con @TypeOf y @typeInfo respectivamente. Sin esta información el puntero anyopaque no podrá ser utilizado.

Hasta ahora, siempre hemos usado algo como self: *TipoConcreto como primer argumento de los métodos de un struct. Seguro que te has dado cuenta de que en este ejemplo usamos self: *@This().

@This() es una función builtin que nos permite obtener el tipo (struct, enum o union) dentro del cual se ha declarado.

Esto es súper útil en este caso, porque el struct es anónimo: no lo hemos declarado como tipo explícito, pero necesitamos tener un puntero a ese tipo para  manejarlo con self.

    // meter el cargador

    fn load_mag(self: *@This(), x_mag: anytype) void {

        // ...

    }

Puntero a función especializada, @ptrCast y @alignCast

En los ejemplos anteriores, las funciones anónimas y encapsuladas, nos servían para declarar partes del código inaccesibles desde fuera de la función que las contiene.

En este caso usamos esta técnica para algo totalmente distinto: esta vez sí queremos que se pueda llamar esta función desde fuera y por eso la guardamos en el puntero self.fn_empty_mag, declarado previamente.

No es posible definir esta función de manera habitual estática si queremos que funcione con cualquier tipo de cargador al que apunte p_magazine.

Al usar anyopaque en su declaración, borramos el tipo de dato, pero una vez llega el momento de usarlo, necesitamos recuperar el tipo real. Lo hacemos a partir de la información contenida en o_tinfo.

    // meter el cargador

    fn load_mag(self: *@This(), x_mag: anytype) void {

        // ...

        // función anónima que sirve para poder procesar un tipo específico

        self.fn_empty_mag = struct {

            fn _(p_magazine: *anyopaque) void {

                const p_mag: *o_tinfo.pointer.child =

                    @ptrCast(@alignCast(p_magazine));

                if (!p_mag.*.b_initialized or

                    p_mag.*.l_ammo.items.len == 0)

                    return print("click? (sin balas)\n\n", .{});

                while (p_mag.*.l_ammo.items.len > 0)

                    print(

                        "{s}! ",

                        .{@tagName(p_mag.*.l_ammo.orderedRemove(0))},

                    );

                print("\n", .{});

            }

        }._;

    }

Conseguimos estos datos usando @alignCast y @ptrCast.

@alignCast

La alineación es una restricción que nos impone el hardware para acceder al dato. Un valor de cierto tipo tiene que empezar en una dirección de memoria que sea múltiplo de N bytes. Esa N es la alineación de ese tipo.

Por ejemplo:

- u8: 1 byte

- u16: 2 bytes

- u32: 4 bytes

- struct (8 o 16 bytes, según el contenido)

Como su alineación es 1 un u8 puede almacenarse en cualquier dirección de memoria. Un u32 debe empezar en múltiplos de 4: 0, 4, 8, 12 etc.

Un struct alineado a 8 tiene que empezar en direcciones múltiplos de 8: 0, 8, 16, 24 etc.

Si intentamos leer un u32 (alineación 4) desde la dirección 3, eso provocará errores, comportamientos indefinidos o un crash.

En Zig, un puntero siempre tiene definida su alineación. Un *anyopaque tiene alineación 1 porque no sabemos que tipo hay detrás, pero el puntero a un struct  tendrá alineación 8, 16 o más dependiendo de su contenido.

Cuando tenemos:

*anyopaque → *MetalMagazine

Sabemos que el puntero anyopaque apunta a un valor que tiene una alineación mayor de 1.

@alignCast cambia la alineación del puntero. Esta alineación es inferida por el tipo de la variable a la que se asigna.

@ptrCast convierte el tipo de puntero al de la variable a la que se asigna, no toca la alineación.

Ninguna de estas dos funciones mueve memoria, ni copia los datos, ni corrige errores.

    const p_mag: *o_tinfo.pointer.child =

                    @ptrCast(@alignCast(p_magazine));

Aquí o_tinfo es la información estructurada que hemos obtenido previamente sobre el tipo.  .pointer.child nos da el tipo exacto del struct al que apuntamos.

Después del incidente en la Plaza Verde, la Santa Inquisición obligó a todos los clérigos a abandonar la ciudad en contra de su voluntad. Alegaron que el demonio se había hecho fuerte y que no podían garantizar la seguridad del clero.

Por suerte, el rey hizo que aquella situación durase poco.

Mientras tanto, Juanelo creó un monje autómata que ayudó a la gente en sus rezos, bodas y bautizos… pero esa ya es otra historia.


Comprobar si existen campos con @hasField
Resumen del capítulo
© 2025 Zen of Zig