한글 번역
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_ptr
이 AcceptFoo()
내부의 “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