Arrays

Alternatives
  • kit !!:

    • "you might want to consider just using a hashset, if the order isn't important and the array is relatively large".

  • Karl Zylinski:

    • It's usually a sign of poor design. Better have an index or handle around and remove using index directly. Any time my code removes by finding the index by element value, then it is a code smell to me.

    • Caio: but every time the array shifts, all indexes stored would have to be updated, no? and what would you use as a handle in this case?

    • Don't remove stuff. Use a free list.

Common Operations

Removing
  • unordered_remove .

    • Faster, but can change the order of elements.

    unordered_remove(&dyn_arr, idx)
    
  • ordered_remove .

    • Doesn't change the order of elements.

    ordered_remove(&dyn_arr, idx)
    
Info
  • len .

    • The len  built-in procedure returns the length of v  according to its type:

      • Array: the number of elements in v.

      • Pointer to (any) array: the number of elements in v^  (even if v  is nil ).

      • Slice, dynamic array, or map: the number of elements in v ; if v  is nil , len(v)  is zero.

      • String: the number of bytes in v

      • Enumerated array: the number elements in v.

      • Enum type: the number of enumeration fields.

      • #soa  array: the number of elements in v ; if v  is nil , len(v)  is zero.

      • #simd  vector: the number of elements in v .

    • For some arguments, such as a string literal or a simple array expression, the result can be constant.

  • cap .

    • The cap  built-in procedure returns the length of v  according to its type:

      • Array: the number of elements in v.

      • Pointer to (any) array: the number of elements in v^  (even if v  is nil ).

      • Dynamic array, or map: the reserved number of elements in v ; if v  is nil , len(v)  is zero.

      • Enum type: equal to max(Enum)-min(Enum)+1 .

      • #soa  dynamic array: the reserved number of elements in v ; if v  is nil , len(v)  is zero.

    • For some arguments, such as a string literal or a simple array expression, the result can be constant.

Fixed Arrays ( [n]T )

  • Fixed Arrays .

  • Similarity to structs :

    • Fixed arrays are equivalent to a struct with a field for each element.

    • They are just a number of values in a row in memory.

Creation and Assigning

some_ints: [7]int
// With inferred size.
some_ints := [?]int{1, 2, 3, 4, 5}

favorite_animals := [?]string{
    // Assign by index
    0 = "Raven",
    1 = "Zebra",
    2 = "Spider",
    // Assign by range of indices
    3..=5 = "Frog",
    6..<8 = "Cat"
}
some_ints[0] = 5
some_ints[3] = 40
some_ints = {5, 4, 3, 1, 2, 98, 100}
    // Since the size is defined as 7, 7 elements must be given.
x := [5]int{1, 2, 3, 4, 5}
for i in 0..=4 {
    fmt.println(x[i])
}

Iterate

for element in some_ints {
    
}       
for element, idx in some_ints {
    
}       
for &element in some_ints {
    element *= 2
}       

Copy

some_ints: [3]f32 = {1, 2, 3}
some_ints2 := some_ints
  • Modifying array 2 will not modify array 1, and vice versa.

  • "No shared memory between fixed arrays".

Fixed Arrays - Small Array

  • Small_Array .

  • Pretty neat.

  • Basically it's a Fixed Array with an API similar to a Dynamic Array.

  • I found it really cool.

  • The Skeleton  uses this, as a reference.

Fixed Arrays - Enumerated Arrays ( [enum]int )

  • Think of it as a Fixed Array.

  • Even though we don't supply the size, the array will be the size of the enum.

Create

// Enum
Nice_People :: enum {
    Bob,
    Klucke,
    Tim
}

// Method 1
nice_rating := [Nice_People]int {
    .Bob = 5,
    .Klucke = 7,
    .Tim = 3,
}

// Method 2: all zeroes
nice_rating := [Nice_People]int 

// Method 3: partial initialization
nice_rating := #partial [Nice_People]int {
    .Klucke = 7,
}

Access

bob_niceness := nice_rating[.Bob]

Slices ( []T )

Creation

  • Via Fixed Array :

    a := [7]int{ 5, 4, 3, 1, 2, 98, 100 } 
        // Left side:  Fixed Array.
        // Right side: Array Literal.
    b := a[2:5]
        // Left side: Slice.
    
  • Via Slice Literal :

    • Implicitly creates a stack fixed array, and then get a reference to it.

    a := []int{ 1, 6, 3 } 
        // Left side:  Slice.
        // Right side: Slice Literal.
    
  • Zero valued :

    • The zero value of a slice is nil . A nil slice has a length of 0 and does not point to any underlying memory. Slices can be compared against nil  and nothing else.

    a: []int
    if a == nil {
        fmt.println("a is nil!")
    }
    
  • Via rawptr  and length :

    • From base:runtime  -> internal.odin

    @(private)
    byte_slice :: #force_inline proc "contextless" (data: rawptr, len: int) -> []byte #no_bounds_check {
        return ([^]byte)(data)[:max(len, 0)]
    }
    

Batch assignment

  • Heap allocated slice :

    • NOT WHAT YOU WANT :

      a := make([]int, 4, context.allocator)
      a = { 10, 20, 30, 40 } 
      
      • This replaces the heap allocated slice with a stack slice.

    • Using copy :

      a := make([]int, 4, context.allocator)
      copy(a, []int{ 10, 20, 30, 40 })
      
    • Using slice.clone :

      a := slice.clone([]int{ 10, 20, 30, 40 }, context.allocator)
      

Destruction

  • You only need to delete the slice if  the underlying value is heap allocated.

  • You could delete the original [dynamic]int  or the []int ; both will delete the same memory.

a := make([dynamic]int, context.allocator)
b := a[:]
delete(b)
    // Will delete the underlying memory of `a` and `b`, as both point to heap memory.

// or

a := make([]int, context.allocator)
delete(a)

Access range

// Everything
a[:]

// From idx 3 to the end
a[3:]

// From start to idx 5
a[:5]

Memory

  • Their length is not  known at compile-time.

  • Slices are like references to arrays; they do not store any data.

    • Internally, a slice stores a pointer to the data and an integer to store the length of the slice.

    • "A window into the array".

  • A slice always points to memory, which can be in the stack or heap. If it's in the stack, no need for manual memory management, but if it's in the heap, we can use the address stored by the slice to free its memory, the same as done by a [dynamic]byte .

  • I can expand a [dynamic]byte , but not a []byte , since a dynamic array has an allocator, and slices don't. Both of them point to memory, but slices can only free it, while [dynamic]byte  can free and expand.

  • No allocation is done when slicing.

    • This means it is bound to the array from which the slice was made.

    • For this reason, it is preferable to pass slices as procedure parameters.

  • Converting Array Slice to Dynamic Array :

    some_ints3 := slice.to_dynamic(some_ints2)
        // array slice to dynamic array
    
  • Allocate memory :

    // Method 1
    some_ints3 := slice.clone(some_ints2)
        // array slice to array slice
    
    // Method 2: with `make`
    some_ints3 := make([]int)
    
    • Delete :

      • If the slice has its own memory, then it is necessary to free this memory afterward:

        delete(some_ints3)
        

Dynamic Arrays ( [dynamic]T )

Creation

// Without make
dyn_arr: [dynamic]int

// With make
dyn_array := make([dynamic]int, context.temp_allocator)
dyn_array := make([dynamic]int, 5, 10, context.temp_allocator) // len 5, cap 10

// With core:bytes lib.
b: bytes.Buffer
bytes.buffer_init_allocator(&b, 0, 2048) // len 0, cap 2048
bytes.buffer_write_string(&b, "my string")

Destruction

delete(dyn_array)

Clear

clear(&dyn_array)

Appending

  • append .

    • n: int  symbolizes the number of elements appended.

    append(&dyn_arr, 5)
        // dyn_arr[0] is now 5
    
    x: [dynamic]int
    append(&x, 123)
    append(&x, 4, 1, 74, 3) // append multiple values at once
    
    y: [dynamic]int
    append(&y, ..x[:]) // append a slice
    
    • Memory considerations when resizing :

      skeleton_add_joint :: proc(skeleton: ^Skeleton, parent_joint_idx: int, pos: eng.Vec2, rot: f32 = 0, scale: f32 = 1.0, name: string = "") -> int {
          if eng.error_assert(skeleton != nil) do return INVALID_JOINT
          
          parent_joint := &skeleton.joints[parent_joint_idx]
              // !! This is invalidated if the append below causes a resize.
         
          append(&skeleton.joints, Joint{
              pos   = pos,
              rot   = rot,
              scale = scale,
              name = name,
              parent = parent_joint,
              skeleton = skeleton,
          })
          
          joint_idx := len(skeleton.joints) - 1
          append(&parent_joint.children, joint_idx)
          return joint_idx
      }
      
      • Barinzaya:

        • If the dynamic array is full when you append something, then it'd need to resize to add one. That may cause the backing memory to move.

        • You're taking parent_joint   before  you append  to the same array, so parent_joint  may be invalid after the append.

        • Also, you're storing pointers to the array in Joint  (the parent  field). Those can also become invalid when the dynamic array resizes

      • Caio:

        • So, what you are saying is that: if I have a pointer to the array, and the array resizes, then I'm screwed? I shouldn't store a pointer to an element in an array?

      • Barinzaya:

        • Basically, yes. Though specifically when it resizes as in reallocates  (i.e. cap  changes). append ing to an array with len == cap  will cause it to reallocate, for instance

        • That can possibly  resize in place, but unless you know the specifics of the allocator it's using, you shouldn't rely on it.

        • If it moves, the whole array will move (i.e. the [dynamic]Joint  will point somewhere else)

        • The [dynamic]Joint  struct itself  won't move, it's still firmly in your struct--but the array's actual data is behind a pointer, and that can move.

      • SaiMoen:

        • Unless you know the pointer is stable  because the backing allocator wouldn't move it (e.g. virtual arena w/ .Static where the only thing using it is the dynamic array).

      • Caio:

        • Is there a rule of thumb for dealing with this situations? some safe design I could use?

      • Barinzaya:

        1. Store array indices, rather than pointers, and any time you append , assume that any pointers you got before the append  are now invalid.

        2. If you were to individually  allocate each Joint  (i.e. using new ), then resizing the array wouldn't move the Joint s themselves, just its array of pointers; by using [dynamic]^Joint .

        3. The other option, if you know how many Joint s there will be, is to reserve  the dynamic array ahead of time. If it never needs to resize, then you won't have an issue. You'd just need to be careful to make sure that it doesn't, in fact, resize. Indices would probably be less error-prone.

        4. "have you looked at relative.Ptr? whether thats an alternative".

          • It would work if all of the pointers are within the same array as they seem to be here, yeah. Though array indices would probably be simpler and more debuggable.

        5. It's handy to have a fixed master array of entities that never changes and serves as a reference, and then you can manipulate seperate arrays of pointers (sorting, growing, etc). However this is not cache-local so it depends on your perf requirements.

  • inject_at .

    • inject

  • assign_at .

    • ?

Iterate

for element in dyn_array {
}       
for element, idx in dyn_array {
}       
for &element in dyn_array {
    element *= 2
}       

Memory

  • Cache :

  • Location :

    • Stored in the heap.

    • They don't "hold" the memory, but actually just point to the address in memory where it is allocated.

  • Allocator :

    • Is where the data the pointer points to comes from and where it goes to realloc.

  • Interacting with Slices :

    • When you slice a dyn array like my_dyn_array[:] , the slice's pointer  and len  will be the same as the my_dyn_array .

      • Because it's the same pointer, when you go to delete it the allocator knows which allocation you want to free.

      • In other words, freeing the array slice means that the original my_dyn_array  is freed, as they both point to the same thing.

  • Growth :

    • "Grows when the capacity  is equal to the length ".

    • It's possible to use a different allocator to make the array grow:

      // Method 1: change the allocator used by the array.
      dyn_arr: [dynamic]int
      dyn_array.allocator = context.temp_allocator
      append(&dyn_array, 5)
      
      // Method 2: use `make` when creating the array
      dyn_array := make([dynamic]int, context.temp_allocator)
      
      // Method 3: change the default allocator of the context (not recommended)
      dyn_arr: [dynamic]int
      context.allocator = context.temp_allocator
      append(&dyn_array, 5)
      
      
  • Copying :

    • Correct method :

      dyn_array: [dynamic]int
      append(&dyn_array, 5)
      
      dyn_array2 := slice.clone_to_dynamic(dyn_array[:])
      
    • Incorrect method :

      • Be careful!

      dyn_array: [dynamic]int
      append(&dyn_array, 5)
      
      dyn_array2 := dyn_array
      
      • The second array points to the same location as the first array.

        • This is extremely error-prone, since appending to the first array will grow it, but the second array will not grow; things like that.

        • It will probably crash.

  • Alternatives :

    1. Via Buffers  from the 'core:bytes' library :

      • Loads information that may or may not be useful:

        • off: int

          • I believe it represents the offset from where reading stopped.

          • This is used everywhere, so if something has been read, it is excluded from all future operations, including bytes.buffer_to_bytes .

          • Fortunately, this is quite explicit when reading the library's procs.

        • last_read: Read_Op

          • Flags for the last thing read.

          Read_Op :: enum i8 {
              Read       = -1,
              Invalid    =  0,
              Read_Rune1 =  1,
              Read_Rune2 =  2,
              Read_Rune3 =  3,
              Read_Rune4 =  4,
          }
          
      • Not intended for sorting, element removal, etc.

        • Obviously possible, since it's fundamentally just a [dynamic]T , but it's not the focus.

Multi-pointer (C Arrays) ( [^]T )

  • Multi-pointers are just C arrays.

  • See C#Arrays  for better understanding.

  • Multi Pointer .

    • "The name may be subject to change."

  • The type [^]T  is a multi-pointer to T value(s).

  • Used in :

    • Describe foreign  (C-like) pointers which act like arrays (pointers that map to multiple items).

    • It is precisely what makes up a Raw_Cstring .

      • A Raw_String  is almost the same thing, but contains a length.

  • Zero Value :

    • nil .

Operations

x: [^]T = ...

x[i]   -> T
x[:]   -> [^]T
x[i:]  -> [^]T
x[:n]  -> []T
x[i:n] -> []T

Re-allocating

  • Caio:

    • hello, if image_pixel: []byte  and image_data: [^]u8 , how can I allocate the values from the image_data  into image_pixels ? I'm doing the following, but I'm getting a UAF.

    size := image_get_size(extent, format)
    image_pixels = make([]byte, size, allocator)
    image_pixels = image_data[:size]
    
  • Barinzaya:

    • All image_pixels = image_data[:size]  is doing is changing image_pixels  to point to image_data 's data, it's not actually copying the data. It sounds like you want copy(image_pixels, image_data[:size])

    • copy  is a built-in procedure that copies elements from a source slice/string src  to a destination slice dst . The source and destination may overlap. Copy returns the number of elements copied, which will be the minimum of len(src) and len(dst).

raw_data()

  • Interacting with Multi-Pointers is easiest using the builtin raw_data()  call which can return a Multi-Pointer.

    • raw_data .

    • Returns the underlying data of a built-in data type as a multi-pointer.

b := [?]int{ 10, 20, 30 }
a: [^]int
fmt.println(a)       // <nil>
a = raw_data(b[:]) 
fmt.println(a, a[1]) // 0x7FFCBE9FE688 20

Discussion

  • Discussion 1 :

    • Is raw_data(my_array_ptr)  the same as &my_array_ptr[0] , if len(my_array_ptr) > 0 ? I find that using &my_array_ptr[0]  is a bit more intuitive when something asks for a [^] , does it make sense?

      • Mostly. &my_array[0]  will invoke bounds checking on slices/dynamic arrays whereas raw_data  won't (and will just return the slice's pointer directly). The types are technically different, but ^T  converts to [^]T  so that often doesn't matter

    • so, no harm no foul on using &myarray[0] ? That makes things much easier to understand

      • Should be fine if you know it won't be empty, but it may incur a bounds check. Otherwise, the result will be the same

  • Discussion 2 :

    property_count: u32
    vk_check(vk.EnumerateInstanceExtensionProperties(nil, &property_count, nil))
    
    properties := make([]vk.ExtensionProperties, property_count)
    vk_check(vk.EnumerateInstanceExtensionProperties(nil, &property_count, raw_data(properties)))
    
    fmt.printfln("property_count: %v, properties: %v", property_count, properties)
    
    • Shouldn't I never use a multi-pointer directly, but only use raw_data  to interface with a foreign api?

      • yeah you normally don't need a multipointer

    physical_device_properties := vk.PhysicalDeviceProperties2{
        sType = .PHYSICAL_DEVICE_PROPERTIES_2
    }
    vk.GetPhysicalDeviceProperties2(device, &physical_device_properties)
    
    • just earlier I had to create an array slice and pass it with a raw_data , but now I'm just using a pointer to a struct, why is that?

      • They're equivalent.

      • Both a pointer and a slice are assignable to a multi-pointer.

      • Multi-pointers are just C arrays.