Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does pattern matching on a union have an unreachable pattern warning?

Given the documentation, I cannot understand why the pattern matching on a union doesn't work properly:

union A {
    a1: i32,
    a2: f32,
}

struct B(A);
let b = B(A { a2: 1.0 });
unsafe {
    match b.0 {
        A { a1 } => println!("int"),
        A { a2 } => println!("float"),
    }
}

Outputs "int" with an unreachable warning.

warning: unreachable pattern
  --> src/main.rs:12:13
   |
12 |             A { a2 } => println!("float"),
   |             ^^^^^^^^
   |
   = note: #[warn(unreachable_patterns)] on by default
like image 707
Ehsan M. Kermani Avatar asked Jan 27 '23 17:01

Ehsan M. Kermani


1 Answers

The entire point of a union is that the compiler doesn't store any information in the union about what type it is; it's completely up to the programmer. Because of this, there's no information for a match to use to decide what type the value is.

Because of this, your code is conceptually equivalent to

struct A {
    a1: i32,
}

let b = A { a1: 42 };

match b {
    A { a1 } => println!("int {}", a1),
    A { a1 } => println!("float {}", a1),
}

There's no case in which the second match arm will ever be executed.

In fact, switching back and forth between the fields is a prime usage of a union:

union A {
    i: i32,
    f: f32,
}

let a = A { i: 42 };
let b = unsafe { a.f };

println!("{}", b);

You may wish to use an enum if you want the compiler to keep track of what variant you have. In some contexts, enums are called tagged unions because that's exactly what they are: a union with a tag alongside to identify what the union contains.

Otherwise, you need to track what type is actually in the union in some other manner. One such way is to implement your own tag:

union A {
    a1: i32,
    a2: f32,
}

struct B {
    is_int: bool,
    data: A,
}

let b = B {
    is_int: false,
    data: A { a2: 1.0 },
};

unsafe {
    match b {
        B {
            is_int: true,
            data: A { a1 },
        } => println!("int {}", a1),
        B {
            is_int: false,
            data: A { a2 },
        } => println!("float {}", a2),
    }
}

The tag can be anything you can match on:

union A {
    a1: i32,
    a2: f32,
}

struct B {
    kind: Kind,
    data: A,
}

enum Kind {
    Int,
    Float,
}

let b = B {
    kind: Kind::Float,
    data: A { a2: 1.0 },
};

unsafe {
    match b {
        B {
            kind: Kind::Int,
            data: A { a1 },
        } => println!("int {}", a1),
        B {
            kind: Kind::Float,
            data: A { a2 },
        } => println!("float {}", a2),
    }
}

I suppose you could even use an enum around the union...

union A {
    a1: i32,
    a2: f32,
}

enum B {
    Int(A),
    Float(A),
}

let b = B::Float(A { a2: 1.0 });

unsafe {
    match b {
        B::Int(A { a1 }) => println!("int {}", a1),
        B::Float(A { a2 }) => println!("float {}", a2),
    }
}
like image 150
Shepmaster Avatar answered Feb 07 '23 17:02

Shepmaster