Trying to use core:container/queue

I don’t yet understand generics in Odin, and Im running into an issue with container_queue

import cq "core:container/queue"

@(test)
queue_works :: proc(t: ^testing.T)
{
    q : cq.Queue(int)
    cq.init(&q, capacity=10)
    cq.push_back(&q, int(1))
    assert(cq.len(&q) == 1)
}
 Error: Cannot determine polymorphic type from parameter: '^Queue($T=int)' to '$Q/Queue($T)' 
        assert(cq.len(&q) == 1) 
                      ^^ 

I’m not sure what is happening here.

2 Likes

In short: len doesn’t take the queue as a pointer. Use cq.len(q).

$Q/Queue($T) means a generic type Q, which is a Queue($T) for some other generic type T. So, Queue(int) is the type you want to give it. For comparison, push_back takes the queue as ^$Q/Queue($T), which means a pointer to some type Q.

3 Likes

Aha, my bad.

While on the topic of pointers, my current understanding is that proc(foo: type) roughly means pass by const reference, while proc(foo: ^type) roughly means pass by mutable reference. Is this notion correct, and if so, does it apply to all types?

1 Like

Not quite.

Passing an instance (with just type) can be a pointer, but it depends on the type. If I recall correctly, it’s internally passed by pointer when the type is larger than 16 bytes. Otherwise, it’s passed by copy. Either way, though, you don’t have to worry about the specifics of it–it doesn’t change the semantics of how you use it. It’s immutable as a proc argument, so you can’t change the value it points to.

Passing a pointer (with ^type) is explicitly a pointer, and the proc can change the value that the pointer points to, so in that sense, it does work sort of like a mutable reference. Be aware, however, that that pointer is only valid as long as the variable it points to is still there. If it points to a local variable, the pointer is only valid until its containing proc returns. If it points to a value in a dynamic array or map, it’s only valid until the data is reallocated, which can happen any time a value is added. If it points at something created with new (or something inside of it), it’s only valid until that data is freed. And so on.

2 Likes

A little demonstration:

import "core:fmt"

Foo :: [16]u8

main :: proc() {
	foo : Foo
	test(foo, &foo)
}

test :: proc (val: Foo, ptr: ^Foo) {
	fmt.println(val[0])
	ptr[0] = 123
	fmt.println(val[0])
}

This will print 0 twice. But if you change Foo to be [17]u8, it’ll print 0 and 123. This is a bit of a leak in the abstraction, but since there’s very little reason to pass the same value by value and by pointer, I’ve never seen it be an issue in practice.

There’s also a #by_ptr tag you can add before an argument name to force it to be passed by pointer internally. It’s mostly meant for making interfaces slightly nicer in foreign functions, but it works anywhere. For example, changing the above to this will result in printing 0 and 123 regardless of the size of Foo.

test :: proc (#by_ptr val: Foo, ptr: ^Foo) { ... }

Note that that doesn’t change the semantics of it. You still can’t change val in the proc.

2 Likes

As a rule of thumb for core:*, procedures take a ^T if the procedure has to modify the value passed to it. This includes appending to a dynamic array, and so forth.

Meanwhile if all it has to do is return part of its state, the parameter is passed directly. Like cq.len here.

There’s an exception when you’re mutating a slice in-place. Passing a slice also gives you the data pointer, so you could for example add 1 to each element without having to pass a ^[]T.

Whereas appending to a dynamic array or inserting into a map does need the pointer to the dynamic array (struct), because although it contains the data pointer, the operation does have to update internal state not accessible through the data pointer.

2 Likes

I will just paste my code, I deleted a lot of lines of code, but what is relevant for the queue is there:

//This is the type you are looking for (probably)
Event_Queue :: queue.Queue(SDL.Event)

@(private = "file")
cached_event_listeners: [dynamic]^Event_Queue 

Subscibe_to_InputEvents :: proc(new_event_listener: ^queue.Queue(SDL.Event)) 
{
    append(&cached_event_listeners, new_event_listener)
}

SDL_HandleInputs :: proc() -> bool 
{
    e: SDL.Event
    for SDL.PollEvent(&e) 
    {
        for el in cached_event_listeners 
        {
            queue.push(el, e)
        }

    }
}
//Usage example:
g_event_queue: Event_Queue
Subscibe_to_InputEvents(&g_camera_event_queue)
//I call this function every frame
proc_that_uses_the_event_queue :: proc() {
    event_queue: ^Event_Queue = &g_event_queue
    for i in 0 ..< queue.len(event_queue^) {
        e := queue.get(event_queue, i)

        #partial switch e.type
         {
            case .KEYDOWN, .KEYUP:
                 //do something 
         }
    }
    queue.clear(&g_event_queue)
}

An addendum to this, since I’ve learned a bit more from recent discussions: the exact behavior is platform dependent. In my case (on amd64_linux), it’s 16, but may be other sizes depending on the platform (e.g. amd64_win64 is 8 instead). Still other platforms (e.g. WASM) don’t do this at all. So, it depends a bit more on the platform than I mentioned (or realized at the time).