[Effective Modern C++] 42. 삽입 대신 생성 삽입을 고려하라
생성 삽입 함수들은 삽입 함수들이 하는 모든 일을 할 수 있다. 게다가 좀 더 효율적으로 수행될 수 있는 경우가 있으며, 적어도 이론적으로는, 덜 효율적으로 수행되는 경우는 결코 없다.
std::vector<std::string> vs;
vs.push_back("xyzzy");
// 해석 ver
vs.push_back(std::string("xyzzy"));
// emplace_back은 완벽 전달을 이용하므로 문자열 리터럴이 vector안의 string생성자에게 그대로 전달된다.
// 임시 객체의 생성과 소멸 비용이 들지않음
vs.emplace_back("xyzzy");
생성삽입이 삽입의 성능을 능가하는 경우
1. 추가할 값이 컨테이너에 대입되는 것이 아니라 컨테이너 안에서 생성된다.
위의 예시에서 첫 코드 블럭의 "xyzzy"를 push_back으로 전달하는 경우이다. 하지만 다음과 같이 배정의 경우, 이동 배정 시 이동 원본이 될 임시 객체를 생성해야 하므로 성능상의 이점은 딱히 없다.
std::vector<std::string> vs;
vs.emplace(vs.begin(), "xyzzy");
노드 기반 컨테이너들은 거의 항상 생성을 통해서 새 값을 추가하며, 표준 컨테이너들은 대부분 노드 기반이다. 노드 기반이 아닌 표준 컨테이너는 std::vector, std::deque, std::string 뿐이다. 그리고 노드 기반이 아닌 컨테이너에서는 emplace_back 이 항상 배정 대신 생성을 이용해서 새 값을 컨테이너에 넣는다고 간주해도 무방하다.
2. 추가할 인수 타입(들)이 컨테이너가 담는 타입과 다르다.
어떤 컨테이너<T> 에 T 형식의 객체를 추가할 때에는 생성 삽입이 삽입보다 빠를 이유가 없다. 그럴 때는 삽입 인터페이스에서도 임시 객체를 생성할 필요가 없기 때문이다.
3. 컨테이너가 기존 값과의 중복 때문에 새 값을 거부할 우려가 별로 없다.
이는 컨테이너가 중복을 허용하거나, 또는 추가할 값들이 대부분 고유한 경우에 해당한다. 중복 제한이 있으면 생성 삽입 구현은 새 값으로 노드를 생성하고, 그것을 기존 컨테이너 노드들과 비교한다. 값이 이미 있으면 생성 삽입이 취소되고, 노드가 파괴되므로, 생성과 파괴 비용이 낭비된다. 이런 노드들은 삽입 함수보다 생성 삽입 함수에서 더 자주 생성된다.
자원관리
std::list<std::shared_ptr<Widget>> ptrs;
// 커스텀 삭제자
void killWidget(Widget* pWidget);
// 삽입 버전
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
// 간단한 버전
ptrs.push_back({new Widget, killWidget});
// 생성 삽입 버전
ptrs.emplace_back(new Widget, killWidget);
예를 들어, 삽입 버전에서,
1. std::shared_ptr<Widget> 생성(temp)
2. push_back 이 temp 를 참조로 받음. temp 복사본을 담을 노드 할당 중 메모리 부족(out-of-memory) 예외 발생
3. 예외가 push_back 바깥으로 전파되며 temp 파괴. 동시에 killWidget 호출에 의해 해당 Widget 도 자동으로 해제
의 순서로 동작이 이루어진다고 하자.
이 때, 메모리 누수는 발생하지 않는다. 그렇다면 emplace_back 버전은 어떨까?
emplace_back 버전이 만약
1. "new Widget" 으로 만들어진 생 포인터가 emplace_back 으로 완벽 전달. 새 목록 노드 생성 중 메모리 부족 예외가 발생하고 할당 실패
2. 예외가 emplace_back 밖으로 전파되며 생 포인터가 사라짐
의 순서로 동작한다면, 결국 그 Widget 의 자원이 누수된다.
즉, 이 경우에는 항목 21 에서 말했듯이, "new Widget" 을 바로 전달하지 말고, 별도의 문장으로 분리하는 것이 바람직할 것이다.
// 삽입 버전
std::shared_ptr<Widget> spw1(new Widget, killWidget);
ptrs.push_back(std::move(spw1));
// 생성 삽입 버전
std::shared_ptr<Widget> spw2(new Widget, killWidget);
ptrs.emplace_back(std::move(spw2));
둘 모두 spw의 생성과 파괴 비용이 발생한다.
explicit 생성자
초기화 방식은 두개가 있다. 하나는 복사 초기화이고 하나는 직접 초기화이다.
explicit생성자에서는 복사 초기화를 사용할 수 없지만 직접 초기화는 사용할 수 있다. std::regex는 const char* 타입의 인수를 받고 explicit로 선언된 생성자를 제공한다.
std::regex r1 = nullptr; // 복사 초기화. 오류! 컴파일 안 됨
std::regex r2(nullptr); // 직접 초기화. 컴파일됨
생성 삽입 함수는 직접 초기화를 사용한다. 따라서 explicit 생성자를 지원한다. 삽입 함수는 복사 생성자를 사용하므로 explicit 생성자를 지원하지 않는다.
따라서 다음의 코드는 둘다 엉터리지만 생성 삽입 함수는 컴파일 오류가 뜨지 않는다.
std::vector<std::regex> regexes;
regexes.emplace_back(nullptr); // 컴파일됨; 직접 초기화에서는 포인터를 받는, explicit으로 선언된 std::regex 생성자를 사용할 수 있다.
regexes.push_back(nullptr); // 오류! 복사 초기화에서는 그런 생성자를 사용할 수 없다.
위에서 emplace_back 으로 nullptr 를 전달한 경우, 프로그램은 미정의 동작 무한열차를 타게 된다. 즉, 생성 삽입 함수를 사용할 때에는 제대로 된 인수를 넘겨주는 데 특별히 신경을 써야 한다는 사실을 알 수 있다.
요약
- 이론적으로, 생성 삽입 함수들은 종종 해당 삽입 버전보다 더 효율적이어야 하며, 덜 효율적인 경우는 절대로 없어야 한다.
- 자원 관리 객체를 컨테이너에 추가한다면, 그리고 자원 획득과 그 자원을 자원 관리 객체로 변환하는 시점 사이에 아무것도 끼어들지 못하게 하라는 조언(항목 21 참고)을 제대로 따른다면, 생성 삽입이 삽입 함수보다 더 나은 성능을 보일 가능성은 별로 없다.
- 생성 삽입 함수는 삽입 함수라면 거부당했을 타입 변환들을 수행할 수 있다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 41. 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라
[Effective Modern C++] 41. 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라
2022.10.23 -
[Effective Modern C++] 40. 동시성에는 std::atomic을 사용하고, volatile은 특별한 메모리에 사용하라
[Effective Modern C++] 40. 동시성에는 std::atomic을 사용하고, volatile은 특별한 메모리에 사용하라
2022.10.23 -
[Effective Modern C++] 39. 일회성(one-shot) 사건 통신에는 void future 객체를 고려하라
[Effective Modern C++] 39. 일회성(one-shot) 사건 통신에는 void future 객체를 고려하라
2022.10.23 -
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
2022.10.23