일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 암시적 변환
- exception
- universal reference
- 반복자
- resource management class
- 영화 리뷰
- std::async
- effective stl
- Smart Pointer
- reference
- 게임
- 오블완
- implicit conversion
- iterator
- 예외
- operator new
- 참조자
- UE4
- lua
- 보편 참조
- more effective c++
- effective modern c++
- 영화
- Effective c++
- 언리얼
- c++
- 티스토리챌린지
- 스마트 포인터
- virtual function
- 상속
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 42. 삽입(insert) 대신 생성 삽입(emplace) 고려하기 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 42. 삽입(insert) 대신 생성 삽입(emplace) 고려하기
김디트 2025. 4. 30. 11:06728x90
항목 42. 삽입 대신 생성 삽입을 고려하라
std::vector<std::string>에 요소 삽입
- 삽입 함수로 넣는 요소의 타입이 늘 std::string인 것은 아니다.
std::vector<std::string> vs;
vs.push_back("xyzzy"); // 문자열 리터널을 삽입한다.
- std::vector의 push_back은 lValue와 rValue에 대해 오버로딩 되어 있다.
template<class T, class Allocator = allocator<T>>
class vector
{
public:
...
void push_back(const T& x); // lValue 버전
void push_back(T&& x); // rValue 버전
...
};
- 컴파일러는 const char[6]과 std::string이 불일치함을 인식한다.
- 이를 해소하기 위해 컴파일러는 문자열 리터럴로 임시 std::string 객체를 생성한다.
- 그리고 그 임시 객체를 push_back에 전달한다.
- 즉, 아래와 같은 코드로 치환된다.
vs.push_back(std::string("xyzzy"));
- 잘 동작하지만, 생성자가 두 번, 소멸자가 한 번 호출된다는 점이 불만이다.
- 동작
- xyzzy로부터 rValue인 임시 std::string 객체(줄여서 temp) 생성(생성자 호출 1회)
- temp가 push_back의 rValue 버전에 전달되어 x에 바인딩된다.
- std::vector를 위한 메모리 안에서 x의 복사본이 생성된다.(생성자 호출 1회)
- push_back의 호출이 종료되며 temp가 파괴된다.(소멸자 호출 1회)
- 세 번째 단계(std::vector가 복사본을 생성)에 문자열 리터럴을 직접 전달할 수 있다면 temp의 불필요한 생성 / 호출을 제거할 수 있지 않을까?
- push_back 대신 emplace_back을 사용하면 해결된다.
생성 삽입(emplace_back) 사용하기
vs.emplace_back("xyzzy"); // 문자열 리터럴을 직접 사용하여 vs 안에서 바로 생성한다.
- emplace_back은 완벽 전달을 이용한다.
- 완벽 전달의 한계들에 부딪히지 않는 한,(항목 30 참조)
- 임의의 타입, 임의의 개수의 인수 조합을 emplace_back에 전달할 수 있다.
vs.emplace_back(50, 'x'); // 'x' 문자 50개로 이루어진 std::string 삽입
- push_back을 지원하는 모든 표준 컨테이너는 emplace_back을 지원한다.
- push_front를 지원하는 모든 표준 컨테이너는 emplace_front를 지원한다.
- insert를 지원하는 모든 표준 컨테이너는 emplace를 지원한다.
생성 삽입 함수의 특징
- 생성 삽입(emplacement) 함수들이 성능이 뛰어난 이유는 인터페이스가 더 유연하기 때문이다.
- 삽입 함수들은 삽입할 객체를 받는다.
- 하지만 샙성 삽입 함수들은 삽입할 객체의 생성자를 위한 인수들을 직접 받는다.
- 물론 삽입 함수처럼 컨테이너에 지정된 타입과 동일한 타입의 인수를 넘길수도 있다.
- 복사 생성자, 이동 생성자의 인수로 동작하기 때문이다.
- 즉, 생성 삽입 함수들은 삽입 함수가 하는 일을 그대로 대체할 수 있다.
std::string queenOfDisco("Donna summer");
// 둘 다 복사 생성한다.
vs.push_back(queenOfDisco);
vs.emplace_back(queenOfDisco);
- 그렇다면 항상 생성 삽입을 사용해도 괜찮을까?
- 그렇진 않다.
- 표준 라이브러리의 구현 상 삽입 함수가 더 빠르게 실행되는 상황도 존재한다.
- 하지만 그런 상황들을 특정짓기는 쉽지 않다.
- 다행히 생성 삽입이 더 바람직할 가능성이 큰 상황에 대한 휴리스틱 추론법은 존재한다.
생성 삽입의 성능이 나은 상황
- 추가할 값이 컨테이너에 배정되는 것이 아니라 컨테이너 안에서 생성된다.
- 도입부의 예제("xyzzy"를 std::vector<std::string> vs에 삽입)가 바로 이 경우이다.
- 하지만 컨테이너 안에서 생성되는지 아닌지는 상황과 컴파일러에 따라 다를 수 있다.
-
// 아래의 경우는 대부분 안에서 string을 만들지 않고, 임시 객체를 만들어 이동 할당한다. // 즉, 생성 삽입의 장점이 사라진다. std::vector<std::string> vs; ... vs.emplace(vs.begin(), "xyzzy"); // vs의 첫 번째 위치에 삽입
- 하지만 노드 기반이 아닌 컨테이너에서는 emplace_back이 항상 할당 대신 생성을 이용해서 새 값을 컨테이너에 넣는다고 간주해도 무방하다.
- 추가할 인수 타입들이 컨테이너가 담는 타입과 다르다.
- 컨테이너<T>에 T 타입 객체를 직접 추가할 때는 생성 삽입이 삽입보다 빠를 이유가 없다.
- 컨테이너가 기존 값과의 중복 때문에 새 값을 거부할 우려가 별로 없다.
- 중복 제한이 있는 경우 일반적으로 생성 삽입 구현은 새 값으로 노드를 생성해서 그것을 기존 컨테이너 노드들과 비교한다.
- 그리고 추가할 값이 없는 경우에 그 노드를 컨테이너에 연결한다.
- 하지만 값이 있이므녀 생성 삽입은 취소되고 임시 노드는 파괴된다.
- 즉 생성, 파괴 비용이 낭비된다.
생성 삽입 시 추가로 고려하면 좋은 사항
- 사항 1. 커스텀 삭제자를 사용해야 하는 경우엔 삽입 함수를 사용하자.
void killWidget(Widget* pWidget); // 커스텀 삭제자
std::list<std::shared_ptr<Widget>> ptrs;
// 삽입 함수를 사용한 경우
ptrs.push_back(std::sahred_ptr<Widget>(new Widget, killWidget));
// ptrs.push_back({ new Widget, killWidget }); 위와 동일한 코드
// 1. new Widget으로 임시 객체(temp) 할당
// 2. push_back이 해당 temp를 참조로 받음.
// 3. temp를 담을 노드를 할당하는 도중 메모리 부족(out-of-memory) 예외 발생
// 4. 예외가 push_back 바깥으로 전파되며 temp 파괴
// 5. temp는 커스텀 삭제자를 통해 적절히 해제된다.
// 즉, 누수가 없이 완벽하게 해결된다.
// 생성 삽입 함수를 사용한 경우
ptrs.emplace_back(new Widget, killWidget);
// 1. "new Widget"으로 만들어진 raw 포인터가 emplace_back으로 완벽 전달
// 2. emplace_back은 새 값을 담을 노드 할당하는 도중 메모리 부족 예외 발생
// 3. 예외가 emplace_back 바깥으로 전파
// 4. raw 포인터가 유실되면서 메모리 누수 발생!!
- 사실 new Widget 같은 표현식을 직접 넘겨주는 건 바람직하지 않다.(항목 21 참조)
- 아래와 같이 작성되어야 한다.
std::shared_ptr<Widget> spw(new Widget, killWidget);
// 삽입 함수
ptrs.push_back(std::move(spw));
// 생성 삽입 함수
ptrs.emplace_back(std::move(spw));
// 이 경우 어떤 방식이든 spw의 생성, 파괴 비용이 발생한다.
- 항목 2. 생성 삽입 함수는 직접 초기화를 사용하므로 explicit 생성자를 지원한다.
std::vector<std::regex> regexes;
regexes.emplace_back(nullptr); // ? 하지만 컴파일 성공
regexes.push_back(nullptr); // 컴파일 에러!!
// std::regex는 C 스타일 문자열(char*)로부터 생성될 수 있다.
// 하지만 explicit으로 되어 있으므로 암시적 변환은 불가능하다.
std::regex r1 = nullptr; // 컴파일 에러!
// push_back은 위처럼 복사 생성자를 사용한다.
// 그럼에도 직접 초기화는 허용한다.
std::regex r2(nullptr); // 컴파일 되지만 미정의 동작!
// emplace_back은 위처럼 직접 초기화를 사용한다.
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 41. 항상 복사되는 매개변수는 값 전달도 고려하기 (0) | 2025.04.29 |
---|---|
[Effective Modern C++] 40. std::atomic, volatile (0) | 2025.04.24 |
[Effective Modern C++] 39. void future 객체 (0) | 2025.04.17 |
[Effective Modern C++] 38. 스레드 핸들 소멸자의 동작 (0) | 2025.04.15 |
[Effective Modern C++] 37. std::thread는 unjoinable하게 (0) | 2025.04.11 |
Comments