캐스팅
캐스팅 (Casting)
타입 캐스팅은 어떤 한 타입의 값을 다른 타입으로 바꾸는 일이에요. 이때 Zig는 두 가지를 나누어서 생각해요. 완전히 안전하고 모호함이 없다고 보장되는 변환은 타입 강제 변환(Type Coercion) 이라는 이름으로 한쪽에 두고, 실수로라도 일어나면 곤란한 변환은 명시적 캐스팅(Explicit Casts) 으로 따로 관리하죠. 또 여러 피연산자의 타입이 주어졌을 때 결과 타입을 정해야 하는 세 번째 경우가 있는데, 이건 피어 타입 결정(Peer Type Resolution) 이라고 불러요.
본문
타입 강제 변환 (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.0을comptime_int로 캐스팅해서@as(comptime_int, 10)을 만들고, 다시@as(f32, 10)으로 캐스팅5를comptime_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.
타입 강제 변환: 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 — 정수 타입끼리 변환
-
@intFromBool —
true를 1로,false를 0으로 변환 -
@backingInt — 열거형이나 packed struct/union의 백킹 정수 값 얻기
-
@intFromError — 에러 코드의 정수 값 얻기
-
@intFromPtr — 포인터의 주소 얻기
-
@ptrFromInt — 주소를 포인터로 변환
-
@ptrCast — 포인터 타입끼리 변환
-
@truncate — 비트를 잘라내며 정수 타입끼리 변환
피어 타입 결정 (Peer Type Resolution)
피어 타입 결정은 이런 자리에서 일어나요.
- switch 표현식
- if 표현식
- while 표현식
- for 표현식
- 블록 안의 여러 break 문
- 일부 이항 연산(binary operations)
이런 종류의 타입 결정은, 모든 피어 타입이 강제 변환될 수 있는 타입 하나를 고르는 방식으로 동작해요. 몇 가지 예를 볼게요.
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.