값 vs 참조 의미론 — 대입하면 복사냐 공유냐 이동이냐
변수를 대입하거나 함수에 넘길 때 값이 복사되는가(copy), 같은 것을 가리키게 되는가(reference), 소유권이 넘어가는가(move). C++이 셋을 손으로 고르던 이 선택을 Go는 '복사 기본', Rust는 '이동 기본'으로 각자 언어 차원의 기본값으로 굳혔다. C++ std::move-after-use 버그를 Rust가 컴파일 타임에 막는 지점까지 대응시킨다.
난이도 중급 · 선행 스택/힙, 메모리 관리 모델을 봤으면 더 좋다.
한 줄 요약
b = a처럼 대입하거나 함수에 인자를 넘길 때 무슨 일이 일어나는가 — 값을 통째로 복사(copy)하거나, 같은 것을 가리키게(reference/share) 하거나, 소유권을 넘기고 원본을 비운다(move). C++은 이 셋을 손으로 골랐고, Go는 복사를 기본값으로, Rust는 이동을 기본값으로 굳혔다.
어떤 문제를 푸는가
1
2
3
Buffer a = load();
Buffer b = a; // b는 a와 같은 것인가, 독립된 복제인가?
b.modify(); // 이게 a에도 영향을 주는가?
이 물음의 답이 의미론(semantics)이다. C++ 개발자는 이미 세 답을 다 안다.
C++에서 출발 — 셋을 손으로 고른다
1
2
3
Buffer b = a; // ① 복사 — 독립된 복제(Rule of Three면 깊은 복사)
Buffer& r = a; // ② 참조 — r과 a는 같은 것. r 수정 = a 수정
Buffer c = std::move(a); // ③ 이동 — a의 알맹이를 c로 넘기고 a는 빈 껍데기
C++은 이 셋을 매번 명시적으로 선택하게 한다. 복사가 비싸면 std::move로 넘기고, 공유하려면 &를 붙인다. → 이동 시맨틱과 스마트 포인터
여기가 착지점. Go와 Rust가 하는 일은 새로운 게 아니라, 이 셋 중 무엇을 “기본값”으로 삼느냐의 선택일 뿐이다. C++에서 손으로 고르던 걸 언어가 대신 정해준 것.
Go — 복사가 기본값
Go는 대입·인자 전달이 전부 값 복사다. C++에서 모든 걸 pass-by-value로 넘기는 것과 같다.
1
2
3
b := a // struct면 통째로 복사 — b와 a는 독립
modify(a) // a의 복사본이 넘어가므로 원본 a는 안 바뀐다
modify(&a) // 바꾸려면 포인터를 명시적으로 넘긴다
아, C++의 pass-by-value가 언어 전반의 기본이 된 거구나. 공유하고 싶으면 C++처럼 포인터(
&a)를 명시한다. 단 함정 — slice·map·channel은 값처럼 보여도 내부가 참조적이다(헤더만 복사되고 알맹이는 공유). C++ 개발자가 “복사했는데 왜 원본이 바뀌지?”로 넘어지는 지점. 발판이 깨지는 곳이니 따로 기억해야 한다.
Rust — 이동이 기본값
Rust는 대입·인자 전달이 기본적으로 이동(move)이다. C++의 std::move가 암묵적 기본이 된 셈이다.
1
2
3
let a = Buffer::load();
let b = a; // 이동 — a의 소유권이 b로 넘어감
// println!("{a}"); // ★ 컴파일 에러! a는 이미 이동돼 무효
Copy 트레이트가 붙은 값싼 타입(정수 등)만 예외적으로 복사되고, 공유하려면 &로 빌린다(borrow).
아, C++
std::move가 기본값이 된 거구나 — 게다가 안전판까지. C++에서std::move(a)뒤에a를 실수로 다시 쓰면 “유효하지만 비워진(valid but unspecified)” 상태라 컴파일은 되고 런타임에 조용히 버그가 된다. Rust는 이동한 값을 다시 쓰면 컴파일 에러로 잡는다. C++의 그 함정을 타입 시스템이 원천 봉쇄한 것. → 메모리 관리 모델의 소유권과 같은 뿌리.
언어별 정리
| 대입·전달 기본 | 공유하려면 | 이동 후 원본 사용 | |
|---|---|---|---|
| C++ | 손으로 선택(복사/이동/참조) | & / 포인터 | 컴파일됨(버그 소지) |
| Go | 복사 | 포인터 &a 명시 | (이동 개념 없음) |
| Rust | 이동(Copy면 복사) | &로 빌림 | 컴파일 에러 |
정리: 세 언어가 다른 게 아니라 C++의 복사/참조/이동 중 어느 걸 기본으로 두느냐가 다를 뿐이다. Go는 복사(단순·예측 가능), Rust는 이동(불필요한 복사 없음 + 소유권 추적). C++을 알면 둘 다 “그중 하나를 기본으로 고정한 것”으로 읽힌다.
스스로 점검
1. Rust의 “이동 기본”을 C++ 개념으로 한마디로 옮기면?
답
std::move가 암묵적 기본값이 된 것. C++에선 Buffer b = a;가 복사지만 Rust에선 이동이라 a가 무효화된다. 공유가 필요하면 C++의 &처럼 Rust도 &로 빌린다.
2. C++ std::move 후 원본 사용과 Rust 이동 후 사용의 차이는?
답
C++은 moved-from 객체가 “유효하지만 비워진” 상태라 컴파일되고 런타임 버그가 될 수 있다. Rust는 이동한 값을 다시 쓰면 컴파일 에러로 막는다.
3. Go에서 struct를 복사했는데 원본이 바뀌는 것처럼 보이면 무엇을 의심하나?
답
그 struct가 slice·map·channel을 품고 있는지. 이들은 값 복사 시 헤더만 복사되고 내부 데이터는 공유되므로, 복사본을 통해 바꿔도 원본에 보인다. 진짜 독립 복제가 필요하면 명시적으로 깊은 복사를 해야 한다.