29. 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라.
move semantics (이동 의미론)는 C++11 의 가장 주요한 기능이다.
컴파일러는 비싼 복사 연산을 비교적 저렴한 이동 연산으로 대체할 수 있다. (가능하면 그렇게 해야한다)
또한 C++98 코드 기반을 C++11 준수하는 컴파일러와 표준 라이브러리로 컴파일하면, 소프트웨어가 자동으로 더 빠르게 실행된다. (컨테이너 등 내부적 로직에서 이동 연산이 사용되므로 자동 개선)
개발자들은 이동 연산에 열광하나, 과장된 부분도 분명 존재한다.
이번 시간에는 이동 의미론에 대해 근거 있는 기대를 가질 수 있게 자세히 알아보자.
move semantics 를 지원하지 않는 형식도 다수 존재한다.
- 특수 멤버 함수 자동 생성 조건에 맞지 않아서, 이동 연산이 자동으로 작성되지 않은 케이스
- ex) 생성자 명시적으로 지정했으나, 이동 연산은 명시적으로 작성되지 않음
- 이동 연산 삭제 등으로 이동 연산 비활성화 되어 있는 케이스
- ex) `=delete; 처리 되어 있음
move semantics 를 지원하나, 성능 효율이 크지 않은 경우도 존재한다.
저렴한 이동 방법이 없는 컨테이너도 있다.
또한 컨테이너의 요소들이 저렴한 이동 연산의 요구 사항을 만족시키지 못하는 경우도 있다.
예시로 C++11 에서 도입된 std::array 는 내장 배열에 STL 인터페이스를 씌운 것이다.
다른 표준 컨테이너처럼 자신의 내용을 힙에 저장하지 않는다.
다른 표준 컨테이너는 본인 자료 멤버에 힙 메모리를 가리키는 포인터만 할당한다.
이 포인터의 존재로 상수 시간에 컨테이너 전체 내용을 이동시킬 수 있다.
원본 포인터를 이동시킬 포인터로 복사하고, 원본은 null 로 설정하면 되기 때문이다.
std::vector 의 예시)
std::vector<Widget> vw1;
auto vw2 = std::move(vw1); // O(1)
vw2 에는 std::vector 의 요소들이 아닌, vw1 포인터가 복사된다.
std::array 객체에서는 이러한 포인터가 없으며, std::array 의 내용은 객체 자체에 저장된다.
std::array 의 예시)
std::array<Widget, 10000> aw1;
auto aw2 = std::move(aw1); // O(N)
aw1 의 각 원소는 aw2 로 이동된다.
Widget이 이동이 복사보다 빠른 형식일 때만, 이 이동 연산이 복사 연산보다 빠를 것이다.
하지만 std::array 에서는 이동, 복사 모두 O(N) 의 동일한 시간 복잡도를 가진다.
따라서 ‘컨테이너를 이동하는 것이 포인터를 복사하는 것만큼 저렴하다’ 는 항상 성립하지 않는다.
std::string 및 Small String Optimization
std::string은 이동에 O(1), 복사에 O(N) 의 시간 복잡도를 가진다.
따라서 이동 연산이 더 빠를 것으로 예상되나, 이 또한 항상 성립하지는 않는다.
문자열 구현에서는 작은 문자열 최적화(Small String Optimization, SSO)를 사용하는 것이 많기 때문이다.
작은 문자열은 std::string 객체 내 버퍼 (스택)에 저장하고, 힙에 할당하지 않는다.
작은 문자열 이동 연산은 복사 연산보다 빠르지 않다.
이동이 빠른 이유는 포인터 하나만 복사하면 되기 때문인데, 작은 문자열 이동은 포인터를 사용하지 않기 때문이다.
SSO 는 많은 프로그램에서 짧은 문자열이 자주 쓰이기 때문에 생긴 기법이다.
마지막으로 move semantics 를 사용할 수 없는 경우도 존재한다.
형식 상 이동 연산을 지원하며, 이동 연산이 더 빠르지만 실제로는 복사 연산이 일어난다.
이는 이동 연산 코드에 예외 안전성을 보장하기 위한 noexcept qualifier 가 없기 때문이다.
std::vector 와 같은 stl의 일부 컨테이너 연산은 강한 예외 안전성을 보장하며, 이동 연산이 예외를 던지지 않음이 확실할 때만 복사 연산을 이동 연산으로 대체한다.
move semantics 의미가 없는 경우
- 이동 연산 없음
- 이동 연산 빠르지 않음
- 이동 사용 불가능 (noexcept 누락)
- 원본 객체가 l-value임
위와 같이 move semantics 의미가 없는 경우에는 복사 연산이 동작한다.
따라서 일반적인 케이스에서 (모든 형식을 알 수 없으므로) 이동 의미론이 없던 C++98 시절처럼 객체의 복사 비용을 가정하고 코드를 작성하는 것이 좋다.
이 때 이동 연산은 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정한다.
하지만 코드가 사용하는 형식, 특징(이동 연산 저렴한지) 등을 알 수 있다면, 이동 연산을 적절한 문맥에서 사용해서 기존의 복사 연산을 더 저렴하게 대체할 것으로 믿어도 안전하다.
'dev > C++' 카테고리의 다른 글
Effective Modern C++ #31 기본 Capture mode를 피해라 (0) | 2025.06.30 |
---|---|
Effective Modern C++ #30 완벽 전달이 실패하는 경우들을 잘 알아두라 (0) | 2025.06.30 |
Effective Modern C++ #28 참조 축약을 숙지하라 (0) | 2025.06.25 |
Effective Modern C++ #27 보편참조 에 대한 중복적재(overload) 대신 사용할 수 있는 기법들을 알아두라 (0) | 2025.06.25 |
Effective Modern C++ #26 보편 참조에 대한 중복적재(overload)를 피하라 (0) | 2025.06.23 |