캐스팅

캐스팅 (Casting)

타입 캐스팅은 어떤 한 타입의 값을 다른 타입으로 바꾸는 일이에요. 이때 Zig는 두 가지를 나누어서 생각해요. 완전히 안전하고 모호함이 없다고 보장되는 변환은 타입 강제 변환(Type Coercion) 이라는 이름으로 한쪽에 두고, 실수로라도 일어나면 곤란한 변환은 명시적 캐스팅(Explicit Casts) 으로 따로 관리하죠. 또 여러 피연산자의 타입이 주어졌을 때 결과 타입을 정해야 하는 세 번째 경우가 있는데, 이건 피어 타입 결정(Peer Type Resolution) 이라고 불러요.

출처: Zig Documentation

본문

타입 강제 변환 (Type Coercion)

타입 강제 변환은 어떤 한 타입이 기대되는 자리에, 다른 타입이 주어졌을 때 일어나요.

test "type coercion - variable declaration" {
    const a: u8 = 1;
    const b: u16 = a;
    _ = b;
}

test "type coercion - function call" {
    const a: u8 = 1;
    foo(a);
}

fn foo(b: u16) void {
    _ = b;
}

test "type coercion - @as builtin" {
    const a: u8 = 1;
    const b = @as(u16, a);
    _ = b;
}
$ zig test test_type_coercion.zig
1/3 test_type_coercion.test.type coercion - variable declaration...OK
2/3 test_type_coercion.test.type coercion - function call...OK
3/3 test_type_coercion.test.type coercion - @as builtin...OK
All 3 tests passed.

타입 강제 변환은 한 타입에서 다른 타입으로 가는 길이 전혀 모호하지 않고, 그 변환이 안전하다고 보장될 때만 허용돼요. 예외가 하나 있는데, 바로 C 포인터(C Pointers) 예요.

타입 강제 변환: 더 엄격한 한정 (Type Coercion: Stricter Qualification)

런타임에서 같은 표현을 갖는 값들은, 한정자(qualifier)가 아무리 중첩되어 있어도 그 한정을 더 엄격하게 만드는 방향으로 캐스팅할 수 있어요.

  • const — const가 아닌 것에서 const로 가는 것은 허용돼요
  • volatile — volatile이 아닌 것에서 volatile로 가는 것은 허용돼요
  • align — 큰 정렬에서 작은 정렬로 가는 것은 허용돼요
  • 에러 집합(Error Sets) 에서 상위 집합(superset)으로 가는 것은 허용돼요

이런 캐스팅은 값의 표현이 변하지 않기 때문에 런타임에서 아무 동작도 하지 않아요(no-op).

test "type coercion - const qualification" {
    var a: i32 = 1;
    const b: *i32 = &a;
    foo(b);
}

fn foo(_: *const i32) void {}
$ zig test test_no_op_casts.zig
1/1 test_no_op_casts.test.type coercion - const qualification...OK
All 1 tests passed.

거기에 더해, 포인터는 const 옵셔널 포인터로도 강제 변환돼요.

const std = @import("std");
const expectEqualStrings = std.testing.expectEqualStrings;
const mem = std.mem;

test "cast *[1][*:0]const u8 to []const ?[*:0]const u8" {
    const window_name = [1][*:0]const u8{"window name"};
    const x: []const ?[*:0]const u8 = &window_name;
    try expectEqualStrings("window name", mem.span(x[0].?));
}
$ zig test test_pointer_coerce_const_optional.zig
1/1 test_pointer_coerce_const_optional.test.cast *[1][*:0]const u8 to []const ?[*:0]const u8...OK
All 1 tests passed.

타입 강제 변환: 정수·부동소수점 확장 (Type Coercion: Integer and Float Widening)

정수(Integers) 는 기존 타입의 모든 값을 표현할 수 있는 정수 타입으로 강제 변환되고, 마찬가지로 부동소수점(Floats) 도 기존 타입의 모든 값을 표현할 수 있는 부동소수점 타입으로 강제 변환돼요.

const std = @import("std");
const builtin = @import("builtin");
const expectEqual = std.testing.expectEqual;
const mem = std.mem;

test "integer widening" {
    const a: u8 = 250;
    const b: u16 = a;
    const c: u32 = b;
    const d: u64 = c;
    const e: u64 = d;
    const f: u128 = e;
    try expectEqual(f, a);
}

test "implicit unsigned integer to signed integer" {
    const a: u8 = 250;
    const b: i16 = a;
    try expectEqual(250, b);
}

test "float widening" {
    const a: f16 = 12.34;
    const b: f32 = a;
    const c: f64 = b;
    const d: f128 = c;
    try expectEqual(d, a);
}
$ zig test test_integer_widening.zig
1/3 test_integer_widening.test.integer widening...OK
2/3 test_integer_widening.test.implicit unsigned integer to signed integer...OK
3/3 test_integer_widening.test.float widening...OK
All 3 tests passed.

타입 강제 변환: 정수에서 부동소수점으로 (Type Coercion: Int to Float)

정수(Integers) 는, 가능한 모든 정수 값이 반올림 없이 부동소수점에 저장될 수 있을 때(즉 정수의 정밀도가 부동소수점의 significand 정밀도를 넘지 않을 때) 부동소수점(Floats) 으로 강제 변환돼요. 안전하게 강제 변환할 수 없는 더 큰 정수 타입은 @floatFromInt으로 명시적으로 캐스팅해야 해요.

Float Type Largest Integer Types
f16 i12 and u11
f32 i25 and u24
f64 i54 and u53
f80 i65 and u64
f128 i114 and u113
c_longdouble Varies by target
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "implicit integer to float" {
    var int: u8 = 123;
    _ = ∫
    const float: f32 = int;
    const int_from_float: u8 = @intFromFloat(float);
    try expectEqual(int, int_from_float);
}
$ zig test test_int_to_float_coercion.zig
1/1 test_int_to_float_coercion.test.implicit integer to float...OK
All 1 tests passed.
test "integer type is too large for implicit cast to float" {
    var int: u25 = 123;
    _ = ∫
    const float: f32 = int;
    _ = float;
}
$ zig test test_failed_int_to_float_coercion.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_failed_int_to_float_coercion.zig:4:24: error: expected type 'f32', found 'u25'
    const float: f32 = int;
                       ^~~

타입 강제 변환: 부동소수점에서 정수로 (Type Coercion: Float to Int)

이 모호한 표현은 컴파일러에게 강제 변환 방법을 두 가지나 남겨 주기 때문에, 컴파일 에러를 내는 것이 적절해요.

  • 54.0comptime_int로 캐스팅해서 @as(comptime_int, 10)을 만들고, 다시 @as(f32, 10)으로 캐스팅
  • 5comptime_float로 캐스팅해서 @as(comptime_float, 10.8)을 만들고, 다시 @as(f32, 10.8)으로 캐스팅
// Compile time coercion of float to int
test "implicit cast to comptime_int" {
    const f: f32 = 54.0 / 5;
    _ = f;
}
$ zig test test_ambiguous_coercion.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_ambiguous_coercion.zig:3:25: error: ambiguous coercion of division operands 'comptime_float' and 'comptime_int'; non-zero remainder '4'
    const f: f32 = 54.0 / 5;
                   ~~~~~^~~

타입 강제 변환: 슬라이스, 배열, 포인터 (Type Coercion: Slices, Arrays and Pointers)

const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectEqualSlices = std.testing.expectEqualSlices;

// You can assign constant pointers to arrays to a slice with
// const modifier on the element type. Useful in particular for
// String literals.
test "*const [N]T to []const T" {
    const x1: []const u8 = "hello";
    const x2: []const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
    try expectEqualStrings(x1, x2);

    const y: []const f32 = &[2]f32{ 1.2, 3.4 };
    try expectEqual(1.2, y[0]);
}

// Likewise, it works when the destination type is an error union.
test "*const [N]T to E![]const T" {
    const x1: anyerror![]const u8 = "hello";
    const x2: anyerror![]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
    try expectEqualStrings(try x1, try x2);

    const y: anyerror![]const f32 = &[2]f32{ 1.2, 3.4 };
    try expectEqual(1.2, (try y)[0]);
}

// Likewise, it works when the destination type is an optional.
test "*const [N]T to ?[]const T" {
    const x1: ?[]const u8 = "hello";
    const x2: ?[]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
    try expectEqualStrings(x1.?, x2.?);

    const y: ?[]const f32 = &[2]f32{ 1.2, 3.4 };
    try expectEqual(1.2, y.?[0]);
}

// In this cast, the array length becomes the slice length.
test "*[N]T to []T" {
    var buf: [5]u8 = "hello".*;
    const x: []u8 = &buf;
    try expectEqualStrings("hello", x);

    const buf2 = [2]f32{ 1.2, 3.4 };
    const x2: []const f32 = &buf2;
    try expectEqualSlices(f32, &[2]f32{ 1.2, 3.4 }, x2);
}

// Single-item pointers to arrays can be coerced to many-item pointers.
test "*[N]T to [*]T" {
    var buf: [5]u8 = "hello".*;
    const x: [*]u8 = &buf;
    try expectEqual('o', x[4]);
    // x[5] would be an uncaught out of bounds pointer dereference!
}

// Likewise, it works when the destination type is an optional.
test "*[N]T to ?[*]T" {
    var buf: [5]u8 = "hello".*;
    const x: ?[*]u8 = &buf;
    try expectEqual('o', x.?[4]);
}

// Single-item pointers can be cast to len-1 single-item arrays.
test "*T to *[1]T" {
    var x: i32 = 1234;
    const y: *[1]i32 = &x;
    const z: [*]i32 = y;
    try expectEqual(1234, z[0]);
}

// Sentinel-terminated slices can be coerced into sentinel-terminated pointers
test "[:x]T to [*:x]T" {
    const buf: [:0]const u8 = "hello";
    const buf2: [*:0]const u8 = buf;
    try expectEqual('o', buf2[4]);
}
$ zig test test_coerce_slices_arrays_and_pointers.zig
1/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to []const T...OK
2/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to E![]const T...OK
3/8 test_coerce_slices_arrays_and_pointers.test.*const [N]T to ?[]const T...OK
4/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to []T...OK
5/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to [*]T...OK
6/8 test_coerce_slices_arrays_and_pointers.test.*[N]T to ?[*]T...OK
7/8 test_coerce_slices_arrays_and_pointers.test.*T to *[1]T...OK
8/8 test_coerce_slices_arrays_and_pointers.test.[:x]T to [*:x]T...OK
All 8 tests passed.

더 보기: C Pointers

타입 강제 변환: 옵셔널 (Type Coercion: Optionals)

옵셔널(Optionals) 의 페이로드 타입과 null은 옵셔널 타입으로 강제 변환돼요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coerce to optionals" {
    const x: ?i32 = 1234;
    const y: ?i32 = null;

    try expectEqual(1234, x.?);
    try expectEqual(null, y);
}
$ zig test test_coerce_optionals.zig
1/1 test_coerce_optionals.test.coerce to optionals...OK
All 1 tests passed.

옵셔널은 에러 유니언 타입(Error Union Type) 안에 중첩되어도 동작해요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coerce to optionals wrapped in error union" {
    const x: anyerror!?i32 = 1234;
    const y: anyerror!?i32 = null;

    try expectEqual(1234, (try x).?);
    try expectEqual(null, (try y));
}
$ zig test test_coerce_optional_wrapped_error_union.zig
1/1 test_coerce_optional_wrapped_error_union.test.coerce to optionals wrapped in error union...OK
All 1 tests passed.

타입 강제 변환: 에러 유니언 (Type Coercion: Error Unions)

에러 유니언 타입(Error Union Type) 의 페이로드 타입과 에러 집합 타입(Error Set Type) 은 에러 유니언 타입으로 강제 변환돼요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coercion to error unions" {
    const x: anyerror!i32 = 1234;
    const y: anyerror!i32 = error.Failure;

    try expectEqual(1234, (try x));
    try std.testing.expectError(error.Failure, y);
}
$ zig test test_coerce_to_error_union.zig
1/1 test_coerce_to_error_union.test.coercion to error unions...OK
All 1 tests passed.

타입 강제 변환: 컴파일 타임에 알려진 숫자 (Type Coercion: Compile-Time Known Numbers)

어떤 숫자가 comptime 에서 대상 타입으로 표현 가능하다는 것이 알려져 있으면, 그 숫자는 그 타입으로 강제 변환될 수 있어요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "coercing large integer type to smaller one when value is comptime-known to fit" {
    const x: u64 = 255;
    const y: u8 = x;
    try expectEqual(255, y);
}
$ zig test test_coerce_large_to_small.zig
1/1 test_coerce_large_to_small.test.coercing large integer type to smaller one when value is comptime-known to fit...OK
All 1 tests passed.

타입 강제 변환: 유니언과 열거형 (Type Coercion: Unions and Enums)

태그가 있는 유니언(tagged union)은 열거형으로 강제 변환될 수 있고, 열거형은 유니언의 필드 중 comptime 에 단 하나의 값만 가질 수 있는 필드(예: void )일 때 유니언으로 강제 변환될 수 있어요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

const E = enum {
    one,
    two,
    three,
};

const U = union(E) {
    one: i32,
    two: f32,
    three,
};

const U2 = union(enum) {
    a: void,
    b: f32,

    fn tag(self: U2) usize {
        switch (self) {
            .a => return 1,
            .b => return 2,
        }
    }
};

test "coercion between unions and enums" {
    const u = U{ .two = 12.34 };
    const e: E = u; // coerce union to enum
    try expectEqual(E.two, e);

    const three = E.three;
    const u_2: U = three; // coerce enum to union
    try expectEqual(E.three, u_2);

    const u_3: U = .three; // coerce enum literal to union
    try expectEqual(E.three, u_3);

    const u_4: U2 = .a; // coerce enum literal to union with inferred enum tag type.
    try expectEqual(1, u_4.tag());

    // The following example is invalid.
    // error: coercion from enum '@EnumLiteral()' to union 'test_coerce_unions_enum.U2' must initialize 'f32' field 'b'
    //var u_5: U2 = .b;
    //try expectEqual(2, u_5.tag());
}
$ zig test test_coerce_unions_enums.zig
1/1 test_coerce_unions_enums.test.coercion between unions and enums...OK
All 1 tests passed.

더 보기: union, enum

타입 강제 변환: undefined

undefined 는 어떤 타입으로든 강제 변환될 수 있어요.

타입 강제 변환: 튜플에서 배열로 (Type Coercion: Tuples to Arrays)

튜플(Tuples) 은 모든 필드가 같은 타입이라면 배열로 강제 변환될 수 있어요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Tuple = struct { u8, u8 };
test "coercion from homogeneous tuple to array" {
    const tuple: Tuple = .{ 5, 6 };
    const array: [2]u8 = tuple;
    _ = array;
}
$ zig test test_coerce_tuples_arrays.zig
1/1 test_coerce_tuples_arrays.test.coercion from homogeneous tuple to array...OK
All 1 tests passed.

명시적 캐스팅 (Explicit Casts)

명시적 캐스팅은 내장 함수(Builtin Functions) 로 수행해요.

  • 명시적 캐스팅 중에는 잘못 쓰면 타입 안전성을 위반할 수 있는 것도 있어요.

  • 어떤 명시적 캐스팅은 언어 수준의 검증(assertion)을 수행해요.

  • 또 어떤 명시적 캐스팅은 런타임에서 아무 동작도 하지 않아요(no-op).

  • @bitCast — 타입은 바꾸되 비트 표현은 유지

  • @alignCast — 포인터의 정렬(alignment)을 더 크게

  • @fromBackingInt — 백킹 정수(backing integer)를 바탕으로 열거형이나 packed struct/union 값을 얻기

  • @errorFromInt — 정수 값을 바탕으로 에러 코드 얻기

  • @errorCast — 더 작은 에러 집합으로 변환

  • @floatCast — 더 큰 부동소수점을 더 작은 부동소수점으로 변환

  • @floatFromInt — 정수를 부동소수점 값으로 변환

  • @intCast — 정수 타입끼리 변환

  • @intFromBooltrue를 1로, false를 0으로 변환

  • @backingInt — 열거형이나 packed struct/union의 백킹 정수 값 얻기

  • @intFromError — 에러 코드의 정수 값 얻기

  • @round, @floor, @ceil, @trunc — 부동소수점에서 정수로 변환

  • @intFromPtr — 포인터의 주소 얻기

  • @ptrFromInt — 주소를 포인터로 변환

  • @ptrCast — 포인터 타입끼리 변환

  • @truncate — 비트를 잘라내며 정수 타입끼리 변환

피어 타입 결정 (Peer Type Resolution)

피어 타입 결정은 이런 자리에서 일어나요.

이런 종류의 타입 결정은, 모든 피어 타입이 강제 변환될 수 있는 타입 하나를 고르는 방식으로 동작해요. 몇 가지 예를 볼게요.

const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;

test "peer resolve int widening" {
    const a: i8 = 12;
    const b: i16 = 34;
    const c = a + b;
    try expectEqual(46, c);
    try expectEqual(i16, @TypeOf(c));
}

test "peer resolve small int and float" {
    // This only works for integer types that can coerce to the float type.
    // Larger integer types will cause a compiler error; no float widening occurs.
    var i: u8 = 12;
    var f: f32 = 34;
    _ = .{ &i, &f };
    const x = i + f;
    try expectEqual(x, 46.0);
    try expectEqual(@TypeOf(x), f32);
}

test "peer resolve arrays of different size to const slice" {
    try expectEqualStrings("true", boolToStr(true));
    try expectEqualStrings("false", boolToStr(false));
    try comptime expectEqualStrings("true", boolToStr(true));
    try comptime expectEqualStrings("false", boolToStr(false));
}
fn boolToStr(b: bool) []const u8 {
    return if (b) "true" else "false";
}

test "peer resolve array and const slice" {
    try testPeerResolveArrayConstSlice(true);
    try comptime testPeerResolveArrayConstSlice(true);
}
fn testPeerResolveArrayConstSlice(b: bool) !void {
    const value1 = if (b) "aoeu" else @as([]const u8, "zz");
    const value2 = if (b) @as([]const u8, "zz") else "aoeu";
    try expectEqualStrings("aoeu", value1);
    try expectEqualStrings("zz", value2);
}

test "peer type resolution: ?T and T" {
    try expectEqual(0, peerTypeTAndOptionalT(true, false).?);
    try expectEqual(3, peerTypeTAndOptionalT(false, false).?);
    comptime {
        try expectEqual(0, peerTypeTAndOptionalT(true, false).?);
        try expectEqual(3, peerTypeTAndOptionalT(false, false).?);
    }
}
fn peerTypeTAndOptionalT(c: bool, b: bool) ?usize {
    if (c) {
        return if (b) null else @as(usize, 0);
    }

    return @as(usize, 3);
}

test "peer type resolution: *[0]u8 and []const u8" {
    try expectEqual(0, peerTypeEmptyArrayAndSlice(true, "hi").len);
    try expectEqual(1, peerTypeEmptyArrayAndSlice(false, "hi").len);
    comptime {
        try expectEqual(0, peerTypeEmptyArrayAndSlice(true, "hi").len);
        try expectEqual(1, peerTypeEmptyArrayAndSlice(false, "hi").len);
    }
}
fn peerTypeEmptyArrayAndSlice(a: bool, slice: []const u8) []const u8 {
    if (a) {
        return &[_]u8{};
    }

    return slice[0..1];
}
test "peer type resolution: *[0]u8, []const u8, and anyerror![]u8" {
    {
        var data = "hi".*;
        const slice = data[0..];
        try expectEqual(0, (try peerTypeEmptyArrayAndSliceAndError(true, slice)).len);
        try expectEqual(1, (try peerTypeEmptyArrayAndSliceAndError(false, slice)).len);
    }
    comptime {
        var data = "hi".*;
        const slice = data[0..];
        try expectEqual(0, (try peerTypeEmptyArrayAndSliceAndError(true, slice)).len);
        try expectEqual(1, (try peerTypeEmptyArrayAndSliceAndError(false, slice)).len);
    }
}
fn peerTypeEmptyArrayAndSliceAndError(a: bool, slice: []u8) anyerror![]u8 {
    if (a) {
        return &[_]u8{};
    }

    return slice[0..1];
}

test "peer type resolution: *const T and ?*T" {
    const a: *const usize = @ptrFromInt(0x12345678);
    const b: ?*usize = @ptrFromInt(0x12345678);
    try expectEqual(a, b);
    try expectEqual(b, a);
}

test "peer type resolution: error union switch" {
    // The non-error and error cases are only peers if the error case is just a switch expression;
    // the pattern `if (x) {...} else |err| blk: { switch (err) {...} }` does not consider the
    // non-error and error case to be peers.
    var a: error{ A, B, C }!u32 = 0;
    _ = &a;
    const b = if (a) |x|
        x + 3
    else |err| switch (err) {
        error.A => 0,
        error.B => 1,
        error.C => null,
    };
    try expectEqual(?u32, @TypeOf(b));

    // The non-error and error cases are only peers if the error case is just a switch expression;
    // the pattern `x catch |err| blk: { switch (err) {...} }` does not consider the unwrapped `x`
    // and error case to be peers.
    const c = a catch |err| switch (err) {
        error.A => 0,
        error.B => 1,
        error.C => null,
    };
    try expectEqual(?u32, @TypeOf(c));
}
$ zig test test_peer_type_resolution.zig
1/9 test_peer_type_resolution.test.peer resolve int widening...OK
2/9 test_peer_type_resolution.test.peer resolve small int and float...OK
3/9 test_peer_type_resolution.test.peer resolve arrays of different size to const slice...OK
4/9 test_peer_type_resolution.test.peer resolve array and const slice...OK
5/9 test_peer_type_resolution.test.peer type resolution: ?T and T...OK
6/9 test_peer_type_resolution.test.peer type resolution: *[0]u8 and []const u8...OK
7/9 test_peer_type_resolution.test.peer type resolution: *[0]u8, []const u8, and anyerror![]u8...OK
8/9 test_peer_type_resolution.test.peer type resolution: *const T and ?*T...OK
9/9 test_peer_type_resolution.test.peer type resolution: error union switch...OK
All 9 tests passed.

더 알아보기 (Learn more)