How to compare *anyopaque to *WellKnownType?

This fails to compile:

const std = @import("std");

const MyStruct = struct {};

pub fn main() void {
    var a: *anyopaque = undefined;
    var b: *MyStruct = undefined;

    if (a == b) {
        std.debug.print("Equal!\n", .{});
    }
}

It says incompatible types: '*anyopaque' and '*MyStruct' But why? I’m just trying to compare the address of each pointer, nothing that requires the pointer types to be equivalent or even known.

I’m trying to interop with some C code, and if *anyopaque is meant to replace *void in C then I’ll need to do this kind of thing a lot.

Have you considered @intFromPtr? Documentation - The Zig Programming Language

if (@intFromPtr(a) == @intFromPtr(b)) { ...
2 Likes

Thanks. That seems to work.

It just seemed kind of unexpected (to me at least) that the compiler would complain instead of just automatically converting *MyStruct to *anyopaque when using the == operator.

You can also explicitly coerce the second argument:

if (a == @as(*anyopaque, b)) {

I don’t understand why it doesn’t do that automatically though.

2 Likes

You know, I think there’s an interesting discussion to be had about that.

I can assign any pointer to an opaque pointer (that’s kind of the point after all)…

So semantically, I think we have an issue here (at least at my first approximation).

Basically… it goes something like this…

Define a type P that is a scalar and regular (equality comparable)
Define a type O that is a scalar and regular and assignable from any P.

Assuming the following is true of scalar identity:
A = B 
A == B -> true

Let p be of type P
let o be of type O
o = p
o == p -> true (this is not currently happening)

It seems to me that regular scalar types that are assignable types should also imply equality. It can be implied because we can say there’s a link through assignment via transitivity… (A → B) and (B → C) implies that (A → C)…

I don’t know how I feel about non-scalar types (we currently use functions for that… like comparing strings)

1 Like

You might be overthinking this. If things are the same, we allow comparison, if not, we don’t. If you’re trying to compare a pointer to an int agaisn’t a pointer to a float, that’s likely a mistake. Anyopaque erases the type, so you can force two things to be comparable, if that’s what you want, but it’s on you to define if this makes sense. When you erase the type, you erase the possibily of the compiler helping you catch bugs.

Unfortunately, that’s not true - here’s an example:

pub fn main() !void {

    var x: i32 = 20;
    var y: i64 = 20;

    if (x == y) {
        std.debug.print("\nComparison worked\n", .{});
    }
    else {
        std.debug.print("\nComparison didn't work\n", .{});
    }
}

In this case, i64 and i32 are not the same but they are comparable. So there is precedence for this in the system. I can get other examples too but I think you get the idea.

1 Like

I get it, but this is just an integer promotion, it’s always safe, while erasing a type is dangerous.

The statement above erases the type information from both pointers:

if (@intFromPtr(a) == @intFromPtr(b)) { ...

We’re comparing the addresses regardless of type. At the moment, the only way I’m aware that we can compare pointers is to completely disregard the type information. In that case, I’m not convinced that type erasure safety is the issue here.

Fundamentally, I get what you’re saying and I’d say that if we wanted to make an issue out of type safety, there’s a reason to do that. But then we have to pick our poison :slight_smile:

1 Like

I see why erasing type info could be dangerous, and I can see why there are benefits to not allowing stuff like

var a: *i32 = undefined;
var b: *f32 = undefined;
if (a == b) {
   // something
}

but automatically converting to *anyopaque when doing a comparison seems consistent with existing rules, considering that this compiles:

fn doit(ptr: *anyopaque) void {
  _ = ptr;
}

pub fn main() void { 
   var x: i32 = 0;
   doit(&x);
}

so there’s already a precedent for automatically converting a pointer with a known type to *anyopaque within Zig.

The only way to compare pointers of different types is to throw away type information, and I think that is exactly what Zig is aiming for. @intFromPtr erases type just as casting to *anyopaque. They would be equivalent in the examples mentioned in this topic. What I meant in my previous posts is that, when erasing the type, be it with @intFromPtr or *anyopaque, we want to be explicit about it, like the example you gave: if (@intFromPtr(a) == @intFromPtr(b))

About functions that take *anyopaque as a parameter, I think this boils down to “what the chances the programmer is making a mistake?”. If you’re comparing an *i32 to an *f32, that’s an odd operation, and you’re likely making a mistake. If you’re not, then make it explicit with one of the methods above. However, a function that takes *anyopaque was probably designed to work on any pointer, so it makes sense to allow the coercion without making it explicit. Obviously, someone could make a function that takes an anyopaque but has restrictions on it, just as someone might be making a valid comparison when doing @intFromPtr(a) == @intFromPtr(b), but we want the design of the language to make it easy to do something that is likely correct, and hard to do something that is likely incorrect, and I think the current way Zig does it achieves the best balance.

2 Likes

Right - comparison between different types of pointers is what I am referring to.

I think we’ll have to agree to disagree - it sounds like our difference here comes down to a vision about what should be convenient. We’re basically in agreement about how the sausage gets made, so to speak.

For instance, I would agree with your point about comparing *i32 and *MyStruct - in this case though, they aren’t assignable (and nor should they be) and shouldn’t be easily comparable.

The issue for me is that types like *anyopaque or void* is that they aren’t useable outside of the fact that they act like a common nexus between different pointer types. Given that fact, I personally would like an ergonomic way to check if it is indeed equal to another address of any other type. Imo, that actually flows directly from why we have it at all.

I don’t usually find myself on this side of the fence, but I think @braddabug and I see it as an ergonomics issue (correct me if I’m wrong).

1 Like
const std = @import("std");

fn eq(a: *anyopaque, b: *anyopaque) bool {
    return a == b;
}

pub fn main() !void {
    var ia: i32 = 5;
    var ib: f32 = 45.232323;
    var a: *i32 = &ia;
    var b: *f32 = &ib;

    std.debug.print("{s}\n", .{if (eq(a, b)) "same" else "different"});
}

Seems plenty ergonomic to me and you actually specify clearly and explicitly what you want. Personally I don’t like operators that do plenty of conversions magically.
Also note that a == b used directly without the function call, doesn’t mention *anyopaque anywhere, so I think it could be confusing to many people why is it coercing to another type?

You could argue that it should only happen if at least one of them is *anyopaque, but again my opinion would be “if this is the only use case why not just define a helper function like eq?”

I don’t like making things syntactically convenient which are very rarely used, that reminds me of javascript and similar languages and the crazy code golf stuff people do in those languages. I find it funny to see when they show off their geeky tricks and shenanigans, but I don’t want to see that stuff in a non joking context. I am not 100% this would lead towards that, but it is the concern I have with it.

2 Likes

Yup, and I can understand that sentiment. At the very least, I can appreciate that we have people such as @LucasSantos91 and yourself who are trying to keep it from going off the rails lol.

3 Likes