값에 의한 전달 파라미터
값에 의한 전달 파라미터 (Pass-by-value Parameters)
함수에 파라미터를 넘길 때, Zig는 값의 크기와 종류에 따라 "복사해서 넘길지" 아니면 "참조로 넘길지"를 알아서 골라요. 파라미터가 바뀔 수 없다는(immutable) 점이 이 자유로운 선택을 가능하게 만드는 핵심 원리예요.
본문
정수(Integers)나 부동소수점(Floats) 같은 기본 타입을 파라미터로 넘기면 그 값이 복사되고, 함수 본문에서는 이 복사본을 사용하게 돼요. 이걸 "값에 의한 전달(passing by value)"이라고 불러요. 기본 타입을 복사하는 건 사실상 비용이 들지 않고, 보통 레지스터 하나를 설정하는 정도밖에 안 돼요.
하지만 struct, union, 배열 같은 타입은 크기에 따라 복사 비용이 크게 늘 수 있어서, 때로는 참조로 전달하는 게 훨씬 효율적일 수 있어요. 이런 타입을 파라미터로 넘길 때 Zig는 두 선택지 사이에서 "값으로 복사해 넘길지, 참조로 넘길지"를 알아서 정하는데, 그 기준은 어느 쪽이 더 빠른지예요. 이게 가능한 이유 중 하나는 파라미터가 기본적으로 불변(immutable) 이기 때문이에요. 값이 바뀔 일이 없으니, Zig가 원할 때 안심하고 참조로 넘겨도 안전하죠.
const Point = struct {
x: i32,
y: i32,
};
fn foo(point: Point) i32 {
// Here, `point` could be a reference, or a copy. The function body
// can ignore the difference and treat it as a value. Be very careful
// taking the address of the parameter - it should be treated as if
// the address will become invalid when the function returns.
return point.x + point.y;
}
const expectEqual = @import("std").testing.expectEqual;
test "pass struct to function" {
try expectEqual(3, foo(Point{ .x = 1, .y = 2 }));
}
쉘에서 이렇게 실행해 보면 테스트가 통과하는 걸 볼 수 있어요.
$ zig test test_pass_by_reference_or_value.zig
1/1 test_pass_by_reference_or_value.test.pass struct to function...OK
All 1 tests passed.
point가 참조일 수도 복사본일 수도 있지만, 함수 본문은 그 차이를 신경 쓸 필요가 없어요. 다만 파라미터의 주소(address)를 취하는 데는 매우 조심해야 해요. 참조로 넘어온 경우, 함수가 반환되면 그 주소가 더 이상 유효하지 않다고(그래도 값으로 취급해야 한다고) 생각하는 게 안전해요.
마지막으로, extern 함수의 경우 Zig는 struct와 union을 값으로 넘길 때 C ABI 규칙을 따르기 때문에, C와 연동하는 코드라고 생각하면 돼요.
더 알아보기
- 값에 의한 전달과 참조에 의한 전달의 차이를 실제로 체감해 보려면, 위 예시의 struct를 아주 큰 배열로 바꿔서 성능이 어떻게 달라지는지 관찰해 봐요.
- 파라미터가 불변이라서 생기는 설계 원칙(예: 함수가 파라미터를 바꾸지 않는다)은 Zig의 스타일에서 자주 만나게 될 테니, 다른 함수 시그니처에서도 그 규칙이 어떻게 적용되는지 살펴보면 좋아요.
- C ABI와 관련된 부분은 실제로 extern 함수를 C 라이브러리와 연동해 볼 때 더 깊게 이해할 수 있어요.