std::any: 언제, 어떻게, 그리고 왜

원문은 std::any: How, when, and why by Casey Carter이며 아래는 이를 요약 정리한 글이다.

임의의 사용자 데이터 저장

달력을 구성 요소로 만들어 다른 개발자가 사용할 라이브러리로 배포한다고 하자. 이 달력으로 가급적 많은 문제를 해결할 수 있도록 연, 월, 주, 일을 사용자가 지정한 임의의 데이터와 함께 사용할 수 있게 하려면 어떻게 설계 해야 좋을까.

C 개발자는 각 데이터 구조에 void*를 추가할 듯하다.

하지만 이 방법엔 분명한 단점이 몇 가지 있다.

실제 가리키는 객체가 Foo인지 여부에 관계 없이 항상 void*Foo*로 변환할 수 있다. 연관된 데이터의 타입 정보가 없으므로 저장한 데이터를 원래 타입으로 접근하는 기본적인 타입 안전성조차 보장할 수 없다.

void*는 스마트 포인터처럼 수명 관리를 하지 않으므로 사용자가 직접 연관 데이터의 수명을 관리해야 한다. 실수는 곧 메모리 누수로 이어진다.

void*로 가리키는 객체는 타입을 알 수 없으므로 라이브러리에서 복사할 수 없다. 사용자가 직접 해야 하며 앞의 예와 마찬가지로 실수는 곧 허상 포인터(dangling pointer), 이중 해제(double free) 또는 누수로 이어진다.

C++ 표준 라이브러리의 shared_ptr을 사용해 void*shared_ptr<void>로 변경하면 수명 관리 문제를 해결할 수 있다.

사용자는 shared_ptr<Foo>를 생성할 수 있고 달력에 shared_ptr<void>로 변환해 저장한 후에도 삭제자(deleter)는 잘 처리한다.

사용자가 값이 아닌 객체를 나타내는 shared_ptr<void>의 복사본을 담고 있는 days/weeks 등을 사용하더라도 복사 문제를 해결할 수 있다. 하지만 shared_ptr<void> 역시 void*와 마찬가지로 연관 데이터의 타입을 추적하지 않으므로 타입 안전성 문제는 해결할 수 없다.

그저 그런 해결책은 도움이 되지 않는다

std::any는 똑똑한 void*/shared_ptr<void>이며 복사 가능한 타입이라면 어떤 값이든 초기화할 수 있다.

shared_ptr처럼 any는 자신이 소멸할 때 담고 있는 값을 적절히 소멸하는 방법을 알고 있다. shared_ptr과 달리 any 객체를 복사할 때 담고 있는 값을 복사하는 방법도 알고 있다.

shared_ptr과 달리 any는 담고 있는 타입을 알고 있다.

따라서 any_cast로 참조를 얻는 예처럼, 담고 있는 타입에 접근할 때 올바른 타입으로 접근할 수 있다.

any가 담고 있는 타입이 확실하지 않고 예외를 피하고 싶으면 포인터를 반환하는 any_cast 메서드로 타입 조회 후 접근할 수 있다.

C++ 표준에서는 예외를 던지지 않는 이동 생성자로 작은 객체를 any 객체의 저장소에 바로 생성해 동적 할당 비용을 피하도록 구현할 것을 장려한다. 하지만 이는 최선의 노력일 뿐 어느 크기 이하는 할당하지 않는다는 그런 보장은 없다. Visual C++ 구현에서는 포인터 크기로 소수라면 할당하지 않는 더 큰 any를 사용하지만 libc++와 libstdc++에서는 포인터 크기로 둘 이상에 해당하는 객체에 대해 할당한다(참조: https://godbolt.org/z/RQd_w5).

어휘 타입을 선택하는 방법 (또는 ‘저장할 타입을 알고 있다면 어떻게?’)

저장할 타입이 반드시 복사 가능해야 한다는 것 말고도 저장할 타입을 안다면 std::any가 적절하지 않을 수 있다. 유연함을 위해 성능 저하를 감수해야 하기 때문이다. 정확한 타입 T를 안다면 std::optional을 사용하는 게 좋다. 저장할 타입이 콜백 같이 항상 시그너처가 특정한 함수 객체라면 std::function을 선택한다. 컴파일 시점에 정해진 여러 타입 집합을 저장해야 한다면 std::variant가 좋은 선택이다.

결론

임의의 타입 객체를 저장해야 할 때면 도구 상자에서 std::any를 꺼내자. 저장할 타입에 관해 안다면 더 적절한 도구가 있다는 것도 기억하자.

 

You may also like...