포스트

값 vs 참조 의미론 — 대입하면 복사냐 공유냐 이동이냐

변수를 대입하거나 함수에 넘길 때 값이 복사되는가(copy), 같은 것을 가리키게 되는가(reference), 소유권이 넘어가는가(move). C++이 셋을 손으로 고르던 이 선택을 Go는 '복사 기본', Rust는 '이동 기본'으로 각자 언어 차원의 기본값으로 굳혔다. C++ std::move-after-use 버그를 Rust가 컴파일 타임에 막는 지점까지 대응시킨다.

값 vs 참조 의미론 — 대입하면 복사냐 공유냐 이동이냐

난이도 중급 · 선행 스택/힙, 메모리 관리 모델을 봤으면 더 좋다.

한 줄 요약

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을 품고 있는지. 이들은 값 복사 시 헤더만 복사되고 내부 데이터는 공유되므로, 복사본을 통해 바꿔도 원본에 보인다. 진짜 독립 복제가 필요하면 명시적으로 깊은 복사를 해야 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.