Item 29. 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정해라


move semantics의 한계

C++11에 추가된 가장 중요한 기능은 move semantics일 것이다.

move semantics를 통해 적절한 상황에서 비용이 큰 복사 연산 대신 비용이 작은 이동 연산을 사용하여 프로그램의 성능을 높일 수 있다.

하지만 move semantics는 우리가 생각한 것보다 효과가 없는 경우가 많다.


move semantics가 효과가 없는 경우

다음과 같은 경우 move semantics는 효과를 발휘하지 못한다.

이동 연산이 없는 경우


C++11에서 사용하는 표준 라이브러리들은 대부분 이동 연산이 추가되었다.

하지만 모든 라이브러리가 C++11의 장점을 취하도록 구현되지 않았다.

C++11에서는 컴파일러가 이동 연산을 지원하지 않는 클래스의 이동 연산자를 자동으로 작성해주지만, 예외 사항들이 존재한다.

  1. 클래스에 복사/이동 연산자, 소멸자 중 하나라도 있으면 컴파일러가 자동으로 이동 연산자를 구현하지 않는다. (Item 17 참고)
  2. 클래스의 멤버 변수나 부모 클래스에 이동 연산자가 삭제되어 있으면 컴파일러가 자동으로 이동 연산자를 구현하지 않는다. (Item 11 참고)

다음과 같은 경우에는 이동 연산이 발생하지 않을 것이므로 프로그램의 성능 또한 향상될 일이 없다.

이동이 더 빠르지 않은 경우


C++11에서 사용하는 표준 라이브러리의 모든 컨테이너들에는 이동 연산이 추가되었다.

하지만 이동 연산이 가능하다고 언제나 복사보다 비용이 저렴한 것은 아니다.

대표적으로 std::array 컨테이너가 있다.

본질적으로 std::array는 내장 배열에 STL 인터페이스를 씌운 것으로 다른 표준 컨테이너들처럼 데이터를 힙 영역에 저장하지 않는다.

std::vector와 같은 대부분의 컨테이너 객체들은 컨테이너의 내용이 저장된 힙 메모리를 가리키는 포인터를 멤버 변수로 가지고 있다.

따라서 이동 연산을 하면 포인터만 옮기면 되기 때문에 O(1)의 시간 복잡도로 연산을 끝낼 수 있다.

std::vector<Widget> vw1;

// vw1이 vw2로 이동, 시간 복잡도 O(1)
auto vw2 = std::move(vw1);

image

하지만 std::array 객체는 객체 자체에 데이터가 저장되기 때문에 포인터가 없다.

즉, 복사 연산과 이동 연산 모두 O(n)의 시간 복잡도를 가진다.

std::array<Widget, 10000> aw1;

// aw1이 aw2로 이동, 시간 복잡도 O(n)
auto aw2 = std::move(aw1);

image

또 다른 경우로 std::string이 있다.

std::stringO(n)의 복사 연산과 O(1)의 이동 연산을 제공하니 이동이 더 빠를 것 같지만 언제나 그렇지는 않다.

문자열을 구현할 때 작은 문자열 최적화(SSO, Small String Optimization)를 사용하는 곳들이 많다.

SSO는 작은 문자열(15자 이내인 문자열)을 힙에 할당하여 저장하지 않고 std::string 객체 안의 버퍼에 저장하는 방식이다.

이러한 SSO 기반 구현을 사용하면 이동 연산이 복사 연산보다 빠르지 않다.

이동이 복사보다 빠른 이유는 포인터 하나만 복사하기 때문인데 이 상황에서는 그럴 수 없기 때문이다.

이동을 사용할 수 없는 경우


표준 라이브러리의 일부 컨테이너 연산들은 강한 예외 안정성을 보장한다.

즉, 해당 이동 연산이 예외를 던지지 않는다는 보장이 있어야만 컴파일러가 이동 연산을 수행하고 그렇지 않으면 복사 연산을 수행한다.

따라서 이동 연산자를 선언할 때 뒤에 noexcept 키워드가 없으면 이동 연산을 사용해도 복사 연산이 수행될 수가 있다.

원본 객체가 lvalue인 경우


rvalue만 이동 연산의 원본이 될 수 있는 경우도 있다.

보편 참조(universal reference)에 std::move를 사용할 경우 lvalue인 원본 객체가 이동하면서 의도치 않은 문제가 발생할 수 있다.

이건 구현의 문제 아닌가…?

class Widget {
public:
  template<typename T>
  void setName(T&& newName) { name = std::move(newName); }

private:
  std::string name;
};

// std::string 타입을 반환하는 팩토리 함수
std::string getWidgetName();

Widget w;

auto n = getWidgetName();

w.setName(n); // 앞으로 사용할지도 모르는 lvalue를 move 시킴


카테고리:

업데이트: