Rust 함수 오버로딩 Nightly 실험, Rust 함수 오버로딩을 C++ FFI에 지금 적용해도 될까
Rust 함수 오버로딩을 C++ FFI 공개 API에 곧바로 넣기에는 아직 이르다. splat 실험을 쓰면 C++의 2인자·3인자 std::hypot 오버로드를 Rust에서 같은 함수 이름으로 자연스럽게 호출할 수 있지만, 이 기능은 RFC가 없고 구현도 완성되지 않았으며 변경되거나 삭제될 수 있다. 지금 허용할 범위는 compiler·interop 도구 개발자의 격리된 Nightly 실험까지다.
2026년 8월 31일 Linux에서 공식 C++ 예제를 최신 Nightly로 실행하고, 같은 입력을 Stable tuple/trait 방식과 비교했다. 호출 결과와 Stable 실패 원인은 확인했지만 성능, ABI 안정성, 생성형 바인딩 도구 통합은 시험하지 않았다. 아래 비교를 따라가면 팀의 Stable 경로는 유지하면서 이 실험을 어디까지 평가할지 정할 수 있다.
20초 핵심 요약
- 무엇:
splat은 tuple/trait로 구분하던 인자 묶음을 일반 함수처럼 여러 인자로 호출하게 만드는 Nightly 실험이다. - 왜: 공개 FFI API가 이 기능에 의존하면 RFC 없는 incomplete feature의 변경·삭제로 Stable 빌드와 장기 유지 계약이 깨질 수 있다.
- 어떻게: 공식 C++ 예제의 5·7 출력, Stable tuple 대조군, E0554 실패를 같은 환경에서 비교하고 Nightly CI 격리 여부를 결정한다.
실험이 바꾼 것은 overload 선택보다 호출 모양이다
Rust에 C++식 overload resolver가 완성된 것은 아니다. 현재 Stable에서도 함수가 tuple 하나를 받고, (T, U)와 (T, U, V)처럼 서로 다른 tuple 타입에 trait을 구현하면 인자 개수와 타입에 따라 구현을 나눌 수 있다. 다만 호출부는 hypot((3.0, 4.0))처럼 tuple을 전달한다.
Nightly의 #[rustc_splat]은 hypot(3.0, 4.0)처럼 전달된 여러 인자를 tuple로 묶어 기존 타입 검사에 넘긴다. 실제 선택은 여전히 tuple 타입별 trait 구현이 맡는다. 따라서 이 실험의 mental model은 “새로운 오버로드 엔진”보다 “기존 tuple/trait 선택 앞에 붙은 호출 문법 변환”에 가깝다. Inside Rust의 공식 예제와 설명, tracking issue #153629
이 차이는 도입 판단에 중요하다. 두 문법에서 같은 5와 7이 나왔다는 결과는 호출부 사용성이 달라졌음을 보여줄 뿐, C++의 복잡한 변환 규칙이나 ABI·성능까지 같다는 뜻은 아니다.
최신 Nightly에서 공식 C++ 예제를 먼저 재현한다
검증 환경은 Linux 7.0.0-1009-aws x86_64, Stable rustc 1.98.0 (88d9e12ae 2026-08-18), Nightly rustc 1.100.0-nightly (fd7ed57df 2026-08-29), g++ 13.3.0, rustup 1.29.0이다. 공식 rustfoundation/overloading-examples 저장소의 commit 4b15668dbdb6f8e5c11e658c61b18c1142dfcfb2를 사용했다.
공식 workspace에서 C++ std::hypot 예제를 실행한다. <example>은 저장소를 받은 로컬 경로다.
$ cargo +nightly run --locked --manifest-path <example>/Cargo.toml -p cpp-hypot-overload --bin cpp-hypot-overload
Finished `dev` profile ...
Running `<example>/target/debug/cpp-hypot-overload`
|(3, 4)| double = 5
|(2, 3, 6)| double = 7
|(3, 4)| int = 5
|(2, 3, 6)| int = 7
[exit 0]

네 호출과 종료 상태 0이 정상 판정의 기준이다. 공식 글이 설명한 2026년 7월 31일 이후 Nightly 조건, 별도 인자 호출과 hypot 결과가 이번 관측과 일치했다. 다만 splat은 매일 바뀔 수 있으므로 “nightly”라는 채널명만 기록하지 말고 통과한 날짜나 toolchain을 CI에 고정해야 한다. Rust Foundation 공식 예제 저장소
Stable 대조군은 같은 결과를 다른 호출 형태로 보존한다
Stable 대조군은 같은 (3, 4)와 (2, 3, 6) 입력을 tuple별 trait 구현에 전달했다. 이 대조군은 Rust-only 최소 코드이며 Stable에서 cpp crate를 거쳐 C++ 함수를 호출하는 구성까지 시험한 것은 아니다.
재현에 사용한 stable_tuple.rs 전체 원문은 다음과 같다.
trait HypotArgs {
fn hypot(self) -> f64;
}
impl HypotArgs for (f64, f64) {
fn hypot(self) -> f64 {
let (x, y) = self;
(x * x + y * y).sqrt()
}
}
impl HypotArgs for (f64, f64, f64) {
fn hypot(self) -> f64 {
let (x, y, z) = self;
(x * x + y * y + z * z).sqrt()
}
}
fn hypot<Args: HypotArgs>(args: Args) -> f64 {
args.hypot()
}
fn main() {
println!("tuple 2 args = {}", hypot((3.0_f64, 4.0_f64)));
println!("tuple 3 args = {}", hypot((2.0_f64, 3.0_f64, 6.0_f64)));
}
$ rustc +stable stable_tuple.rs -o stable_tuple
[exit 0]
$ ./stable_tuple
tuple 2 args = 5
tuple 3 args = 7
[exit 0]
Nightly에서는 hypot(3.0, 4.0)와 hypot(2.0, 3.0, 6.0)로 호출했고, Stable에서는 hypot((3.0, 4.0))와 hypot((2.0, 3.0, 6.0))처럼 tuple 한 개를 넘겼다. 둘 다 5와 7을 냈다. Stable 경로에서 포기하는 것은 일반 함수처럼 보이는 호출 형태다. type inference와 trait 선택이라는 중심 메커니즘은 보존한다.
C++ 바인딩 생성 코드라면 명시적 rename도 선택지다. C++ overload별 Rust 이름에 숫자 suffix를 붙이거나 이름을 따로 정하면 호출부는 장황해질 수 있지만, 불안정 언어 기능 없이 충돌을 드러낼 수 있다. cxx와 Zngur의 현재 workaround 및 trade-off는 Rust-C++ Interop Initiative issue #14에 정리돼 있다.
Stable 실패가 E0554가 아니라 MSRV 오류로 보일 수 있다
Nightly 전용 소스에는 #![feature(splat, tuple_trait)]가 필요하다. 이를 Stable 컴파일러로 직접 검사하면 feature gate에서 중단한다.
$ rustc +stable rust-hypot-overload.rs -o should_not_build
error[E0554]: `#![feature]` may not be used on the stable release channel
--> rust-hypot-overload.rs:2:1
|
2 | #![feature(splat, tuple_trait)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 1 previous error
[exit 1]

여기서 공식 workspace 전체에 cargo +stable을 바로 적용하면 다른 실패가 먼저 나타날 수 있다. 이번 첫 시도는 [email protected] requires rustc 1.99로 Cargo 종료 101을 냈다. 예제 workspace의 rust-version = 1.99 검사가 소스의 feature gate보다 앞서 실행됐기 때문이다.
따라서 오류를 다음처럼 나눠 읽어야 한다.
requires rustc 1.99와 종료 101이면 현재 예제 workspace의 최소 Rust 버전 검사다.- E0554와 종료 1이면 Stable 채널이
#![feature(...)]를 거부한 것이다. - feature gate 자체를 확인하려면 해당 Nightly 소스를
rustc +stable로 직접 검사하거나 Stable용 최소 crate를 사용한다.
두 실패를 같은 원인으로 처리하면 toolchain 버전만 올리고도 splat이 Stable에서 가능해졌다고 오해할 수 있다. Stable/Beta에서 unstable feature flag를 쓸 수 없다는 채널 규칙은 그대로 남는다. Rust Book의 Nightly와 unstable features 설명
프로덕션 경로와 실험 경로를 같은 CI에 묶지 않는다
프로덕션 공개 API와 Stable 지원 라이브러리는 tuple/trait 또는 명시적 rename을 유지하는 편이 맞다. Rust Language Team은 splat을 일반 함수 오버로딩의 확정안이 아니라 실험으로 공개했다. RFC가 없고 incomplete implementation이며, 문법과 구현이 바뀌거나 사라질 수 있다고 명시한다. rustdoc에서 splat 인자가 이름 대신 …로 표시되는 방식도 불안정하다. Inside Rust의 제한 사항
반대로 실제 C++ overload 사례를 다루는 compiler·interop 도구 개발자라면 평가할 이유가 있다. 이때는 CI의 역할을 분리한다.
- Stable tuple/trait 또는 rename 경로는 릴리스의 필수 통과 조건으로 유지한다.
- Nightly job에는 성공한 toolchain 날짜나 commit과 공식 예제 commit을 고정한다.
- Nightly job 실패가 Stable 릴리스를 막지 않게 별도 실험 job이나 브랜치에 둔다.
- 2·3인자 및 필요한 타입 조합의 출력과 종료 상태를 회귀 기준으로 남긴다.
- 최신 Nightly에서 ICE가 나면 먼저 toolchain을 갱신하고, 계속되면 전용 issue나
#t-lang/interopZulip에 사례를 제시한다.
Rust Foundation의 overloading 매크로도 Stable 호환층은 아니다. 이 저장소는 ergonomics를 시험하는 별도 실험이며 대부분 최신 Nightly가 필요하다고 밝힌다. 매크로를 추가해 feature-channel 위험이 사라진다고 판단해서는 안 된다. overloading-macros README
중단할 때는 tuple 호출로 되돌린다
실험을 접는 경로는 짧아야 한다. #[rustc_splat]과 #![feature(...)] 의존성을 제거하고, 함수가 tuple 하나를 받도록 돌린 뒤 호출부를 hypot((x, y)) 형태로 복원한다. Nightly 코드를 별도 브랜치나 선택적 CI job에만 두었다면 Stable 배포 코드를 되돌리지 않고 그 job만 제거할 수 있다.
현재 결과로는 프로덕션 C++ FFI에 채택하지 않는 것이 맞다. 이번 비교에서 확인한 것은 좁은 std::hypot 예제의 호출 사용성과 채널 차이뿐이다. 실제 Crubit·cxx·Zngur 통합, 복잡한 overload 변환 규칙, 성능과 codegen, 함수 포인터, rustdoc 결과가 도입 결정에 필요하다면 각각 후속 검증으로 남겨야 한다.
Stable 경로를 릴리스 기준으로 유지한 상태에서만 Nightly 실험을 시작한다. 공식 예제를 고정 버전으로 재현하고, htmx 4의 불안정 변경 회귀 테스트 전략처럼 회귀 검증을 분리해 두면 언어 실험에 피드백하면서도 공개 API의 약속은 흔들지 않을 수 있다.
제휴·협찬은 없다.