Abseil Tip 55 이름 개수 세기와 unique_ptr


title: “이번 주의 팁 #55: 이름 개수 세기와 unique_ptr” layout: tips sidenav: side-nav-tips.html published: true permalink: tips/55 type: markdown order: “055” —

원래 totw/55로 2013-09-12에 게시됨

작성자: Titus Winters (titus@google.com)

2017-10-20 업데이트됨

빠른 링크: abseil.io/tips/55

“그분을 천 개의 이름으로 알 수 있을지라도, 그분은 우리 모두에게 하나이십니다.” - 마하트마 간디

std::unique_ptr와 이름 개수 세기

일상적으로 “값에 대한 이름”이란 특정 데이터 값을 보유하는, 포인터나 참조가 아닌 값 타입 변수를 의미합니다. (lvalue라고 생각하시면 됩니다.) std::unique_ptr은 특정한 동작 요구 사항을 가지고 있기 때문에, std::unique_ptr에 저장된 값은 반드시 단 하나의 이름만 가져야 합니다.

C++ 표준 위원회가 std::unique_ptr에 적절한 이름을 붙였다는 점을 기억해야 합니다. std::unique_ptr에 저장된 비-null 포인터 값은 어느 시점에서도 단 하나의 std::unique_ptr만이 참조할 수 있어야 합니다. 표준 라이브러리는 이를 강제하도록 설계되었습니다. std::unique_ptr을 사용하는 코드에서 발생하는 많은 컴파일 오류는 std::unique_ptr에 대한 이름을 세는 방법을 이해함으로써 해결할 수 있습니다. 하나의 이름만 허용되며, 동일한 포인터 값에 대해 여러 이름이 있는 것은 허용되지 않습니다.

예제 코드에서 이름 세기

아래 코드에서 각 줄 번호마다 동일한 포인터를 참조하는 std::unique_ptr 이름의 개수를 세어 봅시다. 동일한 포인터 값에 대해 두 개 이상의 이름이 존재하는 줄이 있다면, 그것은 오류입니다!

std::unique_ptr<Foo> NewFoo() {
  return std::unique_ptr<Foo>(new Foo(1));
}

void AcceptFoo(std::unique_ptr<Foo> f) { f->PrintDebugString(); }

void Simple() {
  AcceptFoo(NewFoo());
}

void DoesNotBuild() {
  std::unique_ptr<Foo> g = NewFoo();
  AcceptFoo(g); // 컴파일되지 않습니다!
}

void SmarterThanTheCompilerButNot() {
  Foo* j = new Foo(2);
  // 컴파일은 되지만, 규칙을 위반하여 런타임에 이중 삭제가 발생합니다.
  std::unique_ptr<Foo> k(j);
  std::unique_ptr<Foo> l(j);
}

Simple() 함수에서는 NewFoo()로 생성된 unique_ptrAcceptFoo() 내부의 “f”라는 하나의 이름만 가집니다.

반면에 DoesNotBuild()에서는 NewFoo()로 생성된 unique_ptr이 두 개의 이름을 갖습니다: DoesNotBuild() 내의 “g”와 AcceptFoo() 내의 “f”입니다.

이것이 전형적인 고유성 위반입니다. 실행 중 특정 시점에서 std::unique_ptr (혹은 일반적으로 이동 전용 타입)의 값은 단 하나의 이름만 가져야 합니다. 추가 이름을 도입하는 모든 복사 시도는 금지되며, 컴파일되지 않습니다:

scratch.cc: error: std::unique_ptr<Foo>의 삭제된 생성자를 호출했습니다.
  AcceptFoo(g);

std::move()를 사용한 이름 제거

컴파일러가 이를 잡아내지 못하더라도, SmarterThanTheCompilerButNot()에서 보듯이 std::unique_ptr의 런타임 동작이 문제를 드러낼 것입니다. 여러 std::unique_ptr 이름을 도입하면, 컴파일은 되더라도 런타임에 메모리 문제가 발생할 수 있습니다.

이제, 이름을 제거하는 방법을 살펴보겠습니다. C++11은 이를 위해 std::move()를 제공합니다.

void EraseTheName() {
  std::unique_ptr<Foo> h = NewFoo();
  AcceptFoo(std::move(h)); // `DoesNotBuild` 문제를 std::move로 해결
}

std::move() 호출은 효과적으로 이름을 지우는 역할을 합니다. 즉, 더 이상 “h”를 포인터 값의 이름으로 세지 않아도 됩니다. 이로써 고유 이름 규칙을 충족하게 됩니다: NewFoo()로 생성된 unique_ptr은 처음에는 “h”라는 이름을 가지며, AcceptFoo() 호출 내에서는 “f”라는 하나의 이름만 갖게 됩니다. std::move()를 사용함으로써 “h”에서 값을 다시 읽지 않을 것을 약속합니다. 새로운 값이 할당되기 전까지는요.

요약: 이름 세기는 왜 중요한가?

이름 세기는 lvalue, rvalue 등의 복잡한 개념에 익숙하지 않은 사람들에게 유용한 기법입니다. 불필요한 복사 가능성을 인식하고 std::unique_ptr을 올바르게 사용하는 데 도움이 됩니다. 이름을 세어보고, 너무 많은 이름이 발견되면 std::move를 사용하여 불필요한 이름을 지우세요.

reference

https://github.com/abseil/abseil.github.io/blob/master/_posts/2017-10-20-totw-55.md