티스토리 뷰

 C++11 로 오면서 push_back 과 같은 역할을 하는 함수 emplace_back 이라는 함수가 등장한 것을 볼 수 있다. 왜 이 함수가 등장했는지 어떠한 장점과 단점이 있는지 살펴보자.

std::vector<std::string> vs;
vs.push_back("xyzzy");

 위 코드에서 함수로 전달되는 타입은 std::string 이 아닌 문자열 리터럴이다. 템플릿에 선언된 타입과는 다른 타입이라는 것이다. 컴파일러는 형식 불일치를 인지하고 문자열 리터럴의 형식을 다음과 같이 변환한다.

vs.push_back(std::string("xyzzy"));

 이것이 의미하는 것은 임시 스트링 객체가 생성된다는 것이다. 또 벡터 내부적으로 원소를 집어넣을때 객체를 한 번 더 생성한다. 심지어 함수의 실행이 끝나면 임시 스트링 객체의 소멸자가 호출된다. 내부에서 객체를 생성하는데 굳이 임시 스트링 객체를 생성할 필요가 있을까? 지금의 코드는 매우 비효율적이다.

 emplace_back 의 경우에는 어떨까?

vs.emplace_back("xyzzy");

 push_back 과 코드는 똑같지만 컴파일러가 임시 스트링 객체를 생성하지 않는다. 함수 내부에서 원소를 넣을때 객체 생성 될 뿐이다. 이와 같은 형식을 생성 삽입이라고 부른다. 심지어 문자열 리터럴이 아닌 경우에도 push_back 과 똑같은 역할을 하며, 때로는 더 효율적으로 동작하기도 한다. 그렇다면 항상 생성 삽입을 써도 되는 것일까?

 안타깝게도 그렇지 않다. 삽입 함수가 더 빠르게 실행되는 상황도 존재한다. 여러가지의 요인이 존재하는데 어떤 경우인지 알아보자. 

컨테이너의 내부 구현 차이

 컨테이너에 원소를 추가할 때 어떠한 방식으로 추가하느냐의 차이이다. 객체 생성을 통해 추가할 수도 있고, 이동 배정을 통해 추가될 수 있다. 이동 배정을 위해서는 항상 원본 객체가 필요하다. 그래서 임시 객체가 생성되어 생성 삽입의 장점이 사라지는 것이다. 노드 기반 컨테이너들은 거의 항상 삽입을 통해 추가한다. 그 외에 컨테이너는 3가지 밖에 존재하지 않는다. (std::vector, std::deque, std::string) 이 컨테이너들의 emplace 함수가 배정을 사용하고, emplace_back 의 경우에는 생성을 사용한다. 

인수의 형식과 템플릿 형식이 다른 경우

 앞서 보았던 내용이다. 만일 인수의 형식과 템플릿 형식이 같다면 임시 객체의 생성과 소멸이 일어나지 않기 때문에 생성 삽입이 빠를 이유가 없다.

기존 값과의 중복을 허용하지 않은 경우

 std::map, std::set 과 같이 중복을 허용하지 않은 경우를 의미한다. 해당 조건이 문제되는 이유는 생성 삽입의 경우에 노드를 생성해서 중복을 체크한다. 만약 중복이라면 노드를 파괴시키는데 이는 생성과 파괴 비용을 발생시킨다. 이는 삽입 함수보다 생성 삽입에서 더 자주 발생한다.

생성 삽입의 문제점

 이번에는 생성 삽입의 문제점을 알아보겠다. 다음과 같은 컨테이너가 존재한다고 하자.

std::list<std::shared_ptr<widget>> ptrs;

 그리고 커스텀 삭제자를 통해서 해제되어야 하는 객체 원소를 삽입한다고 가정하자. 이 같은 경우에는 make_shared 함수를 사용할 수 없다. 그렇다면 다음과 같이 구현될 것이다.

// 커스텀 삭제자
void killWidget(Widget* pWidget);

// 첫번째 방식
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));

// 두번째 방식
ptrs.push_back({ new Widget, killWidget });

 첫번째와 두번째 방식 모두 임시 객체가 생성된다. 그런데 push_back 함수를 수행하던 중 메모리가 부족해져 예외가 발생한다고 가정하자. 예외가 발생하면 생성되었던 임시 객체 또한 소멸되며 메모리가 정상적으로 해제된다.

 그런데 만일 emplace_back 을 사용해보자.

ptrs.emplace_back(new Widget, killWidget);

 new Widget 은 오른값이기 때문에 생성된 포인터가 emplace_back 안으로 완벽 전달된다. 이제 메모리가 부족하여 예외가 발생했다고 하자. 예외가 emplace_back 밖으로 전달되며 Widget 에 접근할 수 있는 포인터가 사라지게 된다. 즉 댕글링 포인터가 발생하는 것이다. 이처럼 컨테이너를 다룰때는 효율성 보다 예외 안전성에 보다 신경을 써야한다. 위와 같은 문제를 해결하기 위해서는 외부에서 객체를 생성해서 함수로 전달하는 방법으로 해결할 수 있다.

std::shared_ptr<Widget> spw(new Widget, killWidget);

ptrs.push_back(std::move(spw));
ptrs.emplace_back(std::move(spw));

이 외에도 또 다른 문제점이 있다. 다음의 예를 보자.

std::vector<std::regex> regexes;

regexes.emplace_back(nullptr);  // 정상 동작

regexes.push_back(nullptr);     // 컴파일 에러

 정규표현식들을 저장하는 컨테이너를 생성했다. 그런데 emplace_backpush_back 은 서로 다른 양상을 보인다. 그 전에 nullptr 가 들어가는 것이 정상적으로 들어가는지 생각해보자. nullptr 가 정규표현식이라는 것은 매우 비정상적이다. 어떻게 이런 동작이 가능한걸까?

 정규표현식의 생성자를 먼저 알아보아야 한다. 정규표현식의 생성자 중에서 const char* 를 받는 생성자가 explicit 으로 선언되어 있다. 따라서 다음과 같은 코드는 정상 동작한다.

std::regex r = nullptr;        // 컴파일 에러

regexes.push_back(nullptr);    // 컴파일 에러

std::regex r(nullptr);         // 컴파일 성공

 생성 삽입의 경우에는 첫번째 코드를 수행하는 것과 마찬가지이기 때문에 컴파일 에러가 발생하는 것이고 삽입 생성의 경우에는 마지막 라인을 수행하는 것이여서 컴파일에 성공한다. 

 

참고 서적

스콧 마이어스, Effective Modern C++
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함