# Understanding when passing a pointer to a struct is needed and when its not

**URL:** <https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997>\
**Category:** Explain\
**Tags:** language\
**Created:** [May 7, 2025, 12:32am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997 "2025-05-07T00:32:12Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![saurabh](https://ziggit.dev/user_avatar/ziggit.dev/saurabh/32/4623_2.png) [@saurabh](https://ziggit.dev/u/saurabh)\
**Post date:** [May 7, 2025, 12:32am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/1 "2025-05-07T00:32:12Z")

</div>

I was solving [this problem](https://leetcode.com/problems/insert-interval/) in Zig and came up with the below function signature **(I’m not sure of its correctness)**:

```zig
fn insertInterval(alloc: std.mem.Allocator, intervals: []Interval, new_interval: *Interval) ![]Interval {
    // implementation
}

```

I tried declaring the third function parameter (`new_interval: *Interval`), without a pointer, but running unit tests gave errors.

I was able to get around by changing things based on errors, but dont understand it clearly:

- Why does the third parameter have to be `new_interval: *Interval` instead of `new_interval: Interval`

- Why does the second parameter `intervals: []Interval` work without a pointer.

* * *

Also wanted to know if there’s a better way to go about it ?

> **Implementation & Tests**
>
> ```zig
> const std = @import("std");
> const testing = std.testing;
> const test_alloc = testing.allocator;
> 
> const Interval = struct {
> start: u16,
> end: u16,
> };
> 
> fn insertInterval(alloc: std.mem.Allocator, intervals: []Interval, new_interval: *Interval) ![]Interval {
> var periods: std.ArrayListUnmanaged(Interval) = .empty;
> 
> for (0..intervals.len) |i| {
> if (new_interval.end < intervals[i].start) {
> try periods.append(alloc, new_interval.*);
> new_interval.* = intervals[i];
> } else if (new_interval.start > intervals[i].end) {
> try periods.append(alloc, intervals[i]);
> } else {
> new_interval.*.start = @min(new_interval.start, intervals[i].start);
> new_interval.*.end = @max(new_interval.end, intervals[i].end);
> }
> }
> 
> try periods.append(alloc, new_interval.*);
> 
> return periods.toOwnedSlice(alloc);
> }
> 
> test "new interval overlaps with the first interval" {
> var initial = [2]Interval{
> .{ .start = 1, .end = 3 },
> .{ .start = 6, .end = 9 },
> };
> 
> var new = Interval{ .start = 2, .end = 5 };
> 
> const expected = [2]Interval{
> .{ .start = 1, .end = 5 },
> .{ .start = 6, .end = 9 },
> };
> 
> const result = try insertInterval(test_alloc, &initial, &new);
> defer test_alloc.free(result);
> 
> try testing.expectEqualSlices(Interval, &expected, result);
> }
> 
> test "new interval overlaps with three current intervals" {
> var initial = [5]Interval{
> .{ .start = 1, .end = 2 },
> .{ .start = 3, .end = 5 },
> .{ .start = 6, .end = 7 },
> .{ .start = 8, .end = 10 },
> .{ .start = 12, .end = 16 },
> };
> 
> var new = Interval{ .start = 4, .end = 8 };
> 
> const expected = [3]Interval{
> .{ .start = 1, .end = 2 },
> .{ .start = 3, .end = 10 },
> .{ .start = 12, .end = 16 },
> };
> 
> const result = try insertInterval(test_alloc, &initial, &new);
> defer test_alloc.free(result);
> 
> try testing.expectEqualSlices(Interval, &expected, result);
> }
> 
> ```

---

<div class="post-metadata">

**Author:** ![Zambyte](https://ziggit.dev/user_avatar/ziggit.dev/zambyte/32/4977_2.png) [@Zambyte](https://ziggit.dev/u/Zambyte)\
**Post date:** [May 7, 2025, 1:39am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/2 "2025-05-07T01:39:07Z")

</div>

See:

> **[Documentation - The Zig Programming Language](https://ziglang.org/documentation/0.14.0/#Pass-by-value-Parameters)**

Arguments are immutable. If you wish to mutate a value passed as an argument, you must pass a pointer to the value instead. Then you can mutate the dereferenced value.

the obvious answer then as to why you don’t need a `*[]Interval` for intervals is because you’re not mutating it. However, a slice _is_ a pointer (and a length), and you would be able to modify the values pointed to by the slice. You would only require a `*[]Interval` to modify the slice itself (the length value and where the pointer points to).

---

<div class="post-metadata">

**Author:** ![Zambyte](https://ziggit.dev/user_avatar/ziggit.dev/zambyte/32/4977_2.png) [@Zambyte](https://ziggit.dev/u/Zambyte)\
**Post date:** [May 7, 2025, 1:45am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/3 "2025-05-07T01:45:19Z")

</div>

Also see:

> **[Documentation - The Zig Programming Language](https://ziglang.org/documentation/0.14.0/#Type-Coercion-Slices-Arrays-and-Pointers)**

When you pass `&initial` as an argument, you are coercing the array to a slice.

---

<div class="post-metadata">

**Author:** ![squeek502](https://ziggit.dev/user_avatar/ziggit.dev/squeek502/32/409_2.png) [@squeek502](https://ziggit.dev/u/squeek502)\
**Post date:** [May 7, 2025, 2:38am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/4 "2025-05-07T02:38:24Z")

</div>

> [@saurabh](#):
>
> Why does the second parameter `intervals: []Interval` work without a pointer.

Just to clarify, `[]Interval` _is_ a pointer type ([a slice is a pointer + a length](https://ziglang.org/documentation/master/#Slices)).

---

<div class="post-metadata">

**Author:** ![\_sh](https://ziggit.dev/user_avatar/ziggit.dev/_sh/32/8461_2.png) [@\_sh](https://ziggit.dev/u/_sh)\
**Post date:** [May 7, 2025, 5:42am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/5 "2025-05-07T05:42:17Z")

</div>

Q: When is passing a pointer to a struct needed?  
A1: When you need to modify it to be used in a higher scope.  
A2: When you need to make a comparison using its address. E.g. say you wanted to know if the given `*Interval` is in the `[]Interval` slice (by reference, not value).

Q: When don’t you need to pass a pointer to a struct?  
A1: You only care about its value, not its reference. E.g. if you only needed to know if there was an existing `Interval` in an `[]Interval`, but nothing is modified.  
A2: You want to make a copy, or you want to ensure that no changes are made to the locally scoped parameter.

---

<div class="post-metadata">

**Author:** ![ericlang](https://ziggit.dev/user_avatar/ziggit.dev/ericlang/32/8515_2.png) [@ericlang](https://ziggit.dev/u/ericlang)\
**Post date:** [May 7, 2025, 9:13am UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/6 "2025-05-07T09:13:36Z")

</div>

In addition: when it is a big struct and you are only interested in the value use:  
\*const Interval. That’s what I do.

Or not? What is the state of affairs around “passing by value or reference optiimization”?

---

<div class="post-metadata">

**Author:** ![Justus2308](https://ziggit.dev/user_avatar/ziggit.dev/justus2308/32/5305_2.png) [@Justus2308](https://ziggit.dev/u/Justus2308)\
**Post date:** [May 7, 2025, 12:36pm UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/7 "2025-05-07T12:36:12Z")

</div>

I think it’s always safe to just pass the struct by value if you only want to read its fields and don’t need their original addresses. If the compiler decides that it’s too large to copy around it will just use a reference anyways.

In this little test the compiler even turns `bsByValue` into a call to `bsByConstRef`:

```zig
const BigStruct = extern struct {
    header: [100]u64,
    important_data: [100000]u32,

    pub fn byValue(bs: BigStruct, header_idx: usize) u32 {
        const data_idx = bs.header[header_idx];
        return bs.important_data[data_idx];
    }
    pub fn byConstRef(bs: *const BigStruct, header_idx: usize) u32 {
        const data_idx = bs.header[header_idx];
        return bs.important_data[data_idx];
    }
};

extern var big_struct: BigStruct;

export fn bsByValue(header_idx: usize) u32 {
    return big_struct.byValue(header_idx);
}

export fn bsByConstRef(header_idx: usize) u32 {
    return big_struct.byConstRef(header_idx);
}

```

([godbolt](https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1AAvPMFJL6yAngGVG6AMKpaAVxYMQAJlIOAMngMmABy7gBGmMTeAMykAA6oCoS2DM5uHt7xickCAUGhLBFRXrGWmNYpQgRMxARp7p4%2BZRUCVTUEeSHhkTEW1bX1GU39HYFdhT0lAJQWqK7EyOwcaAwKBADUAEJmVcSu1usApNEAIuuYqgSRDOtreweHAOybhxoAguuf6wiYTFhRRwArJsuBoNIdASdXAA2aSvD5fPAsBK1QwEAD66CY1RAQJBYIJEKh0R88PhX3WcVcYXWVBuYQAngA1MSuTAQMIKXHbYC7fYEUjfX7/dF4dDqdauJLGTBTSUko7PckUr4rNbrLHVUXio6ndacgB0Pz%2BkQhm2NIrFqiJxxe7xVX2ImAI83pCgNSJR1UEmOxTDNmqY2utkNtys%2BTxO4cp1Np9IZzlWBAASpgqByuesAFRqjY8vnWQUWyLB3FSvAyuWuBVPO0Ih25jV%2B4O6s6G4vEM0d4M26J1h2fJ0u4huj3IkjejGBgPNq29/tfSNkx5Rvtk94XK4j9YANxq%2BrM6Lu/O5OwI9wIYfe8IuXrj%2BoUm2ZrPZ3atZelsvlXkVC8HztdA9gCPc9%2BQNRkWTcV9hRLK0pivN4l2vDdVDvOkHyfRM1lTdM33FD8Ky/asf1raMh0AsJD2PaxwITARsLTCA8NUeC12vFcOBmWhOEBXhPA4LRSFQTgAC0zFuOYFkwI4Sh4UgCE0TiZgAaxAQENH0ThJD4xShM4XguQ0hSBM40g4FgGBEBQVBkToSJyEoNBbPoKJjA0KQNJoWgty5DldMo5hiAZTg5ICmoGQAeTCbRymMuSnLYQQIoYWhgpM0gsDCVxgEcMRaC5bheCwFhDGAcR0vwJ0Kh3TACsEi5ylcK4Qt4QIrm49LaDwMJiHC5wsF088kRa0gauIMJEkwE5MBKowuqMRSZioAxgAUJk8EwAB3CK4kYEb%2BEEEQxHYKQZEERQVHUdLdC4fRSpAUxzC6zl4BmVA4hsejOAAWgi9ZU3oJglAAMSBgheFQMbiDFWrXosTArE%2BzwIAcQZPFu/wxgKIo9ASJIkbR3HsiRzpsZ6W7miRtoBhcBo9EpyoRlJ7oogpkZCbZ9pmYmVmZgUSTFgkLieJ09LhI4dZHvWDQDSkA0NHWCBcEIEgZOiLgpl4YytCmFS1I0jrtNIFgQGhLwDS8ABOS2uGiSRHjtkpLYADlIfjBPFgyQCMxazMspiQDmAgqQFCgICcuI7OIYJWCWKWZbl%2Br8CIaH0D0A7hFEcRTozi61F0m7SE23q4ha4WOF4t3dPFiKmpD9ZUCoSXxITyR5cV5xnMiNWNa1xaZg7Shy6Nk3okeA1HjBZ3oUBLwZ8eaENB8d2If0ixvfk/vSFU9TNI4aJRY9tftaUvevEP1eOD7kzddGyIkjsSQgA%3D))

I personally only use `*const ...` when I need to read a value in the struct from its original address and a copy wouldn’t produce correct code (for example `*const std.atomic.Value(usize)`).

That way intent is always clear:

- `fn (t: T)` → only needs to read raw value
- `fn (t: *const T)` → needs to read value from its original address
- `fn (t: *T)` → will potentially modify `t`

---

<div class="post-metadata">

**Author:** ![dpirch](https://ziggit.dev/user_avatar/ziggit.dev/dpirch/32/5321_2.png) [@dpirch](https://ziggit.dev/u/dpirch)\
**Post date:** [May 7, 2025, 2:53pm UTC](https://ziggit.dev/t/understanding-when-passing-a-pointer-to-a-struct-is-needed-and-when-its-not/9997/8 "2025-05-07T14:53:49Z")

</div>

> [@Justus2308](#):
>
> If the compiler decides that it’s too large to copy around it will just use a reference anyways.

But often it cannot do this because this reference might then alias something else. For example someone could call a function like `fn insertInterval(alloc: Allocator, intervals: []Interval, new_interval: Interval) ![]Interval` with a `new_interval` that is an element of the passed `intervals` slice. If new\_interval is passed by value, it is a copy and remains constant in the function even if the function changes the elements of the slice. But if the compiler secretly replaces it with a reference, then the value of new\_interval will suddenly change during the function and the behavior of the function might change.

I think this pass-by-reference-optimization will only be done for “pure” functions in the future (according to [eliminate hidden pass-by-reference footguns · Issue #5973 · ziglang/zig · GitHub](https://github.com/ziglang/zig/issues/5973#issuecomment-2380332493)), so for big structs it is a good idea to pass them by reference (`*const Interval`) explicitly, maybe also with `noalias`.
