- 값전달 기법, 생성 삽입 기능을 고려하여라
항목41: 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값전달을 고려하라
- 함수 매개변수를 저장하는 경우가 있음
- 아래 코드의 addName은 자신의 매개변수를 vector에 저장함
1
2
3
4
5
6
7
8
9
10
11
class Widget {
public:
void addName(const std::string& newName) // take lvalue;
{ names.push_back(newName); } // copy it
void addName(std::string&& newName) // take rvalue;
{ names.push_back(std::move(newName)); } // move it; see
… // Item 25 for use
// of std::move
private:
std::vector<std::string> names;
};
위와 같은 코드는 본질적으로 같은 일을 하는 함수를 두 개 작성한 것
- 인라인화되지 않는다면, 목적 코드에 실제로 두 개의 함수가 존재하게됨
한가지 대안
- 보편참조 사용하여 함수 템플릿으로 만들기
- 문제점: 보편참조로 전달 못하는 인수 타입 (항목30)
- 문제점: 디버깅하기 어려워짐 (항목27)
- 문제점: 변환 가능한 타입들에 대해서도 인스턴스화(항목25)
- 보편참조 사용하여 함수 템플릿으로 만들기
규칙 포기
- 사용자 정의 타입의 객체를 값으로 전달하지 말라는 규칙을 포기
1
2
3
4
5
6
class Widget {
public:
void addName(std::string newName) // take lvalue or
{ names.push_back(std::move(newName)); } // rvalue; move it
…
};
std::move(newName)
- newName을 이동
- 복사본으로 완전히 독립적인 객체
- 마지막 사용
- newName을 이동
- C++98에서는 값 전달의 비용이 컸음
- C++98에서는 호출자가 무엇을 넘겨주든, 매개변수 newName이 복사 생성에 의해 생성
- C++11에서는 newName은 인수가 왼값일 때만 복사 생성에 의해 생성, 오른값일 때는 이동 생성에 의해 생성
접근방식 3가지 효율성
왼값, 오른값 중복적재 (참조 전달)
- 복사, 이동 연산을 기준으로 한 비용은 없음
- 왼값: 복사1회
- 오른값: 이동 1회
보편참조 (참조 전달)
- 참조이기에 비용없는 연산
- 왼값: 복사 1사
- 오른값: 이동 1회
- 만약 생성자의 파라미터로 들어가는 값 (std::string 과 literal string)이면 복사 or 이동 연산이 0회 이상
값전달
- 매개변수가 반드시 생성됨
- 왼값: 복사 생성 1회, 이동 1회
- 오른값: 이동 생성 1회, 이동 1회
값전달 추가비용
이동전용 타입에서는 복사 생성이 비활성
std::unique_ptr<std::string>&& ptr
으로 매개변수 설정해야 효율적- 값전달일경우 이동 2회, 참조전달일경우 이동 1회
이동이 저렴한 매개변수에서만 고려
- 이동연산이 추가로 일어나기 때문
항상 저장되는 매개변수에 대해서만 고려
- 그 값을 받고 저장하지 않아도 생성과 파괴 비용 유발
함수가 매개변수를 복사하는 두 가지 방식
1
2
3
4
5
6
7
8
9
10
11
class Password {
public:
explicit Password(std::string pwd) // pass by value
: text(std::move(pwd)) {} // construct text
void changeTo(std::string newPwd) // pass by value
{ text = std::move(newPwd); } // assign text
…
private:
std::string text; // text of password
};
생성을 통한 복사
- Pssword()
- 값전달을 사용하면 다른 대안들에 비해 이동이 한 번 더 수행
- Pssword()
= 연산자를 통한 복사(복사 연산자, 이동 연산자)
- 대입 기반 매개변수 복사의 비용은 대입에 관여하는 객체의 값에 의존(std::string, std::vector)
1
2
std::string newPw = "Sssfdfsdfdfsdfsdfss";
p.changeTo(newPw):
changeTo()
- 왼값이 전달담
- string의 복사 생성자 호출 => 메모리 할당 (새 패스워드 담을 메모리)
- 이동대입 => text가 차지하고 있던 메모리가 해제
- 동적 메모리 관리동작 2번
- 왼값이 전달담
만약 기존 패스워드가 새 패스워드보다 길다면?
- 왼값참조를 사용하면, 메모리 할당 및 해제할 필요 없음
- 그 반대의 경우는 값전달이 참조전달과 거의 비슷할 것
1
2
3
4
5
void changeTo(const std::string& newPwd) // the overload
{ // for lvalues
text = newPwd; // can reuse text's memory if
// text.capacity() >= newPwd.size()
}
정리
대입연산을 통해서 복사하는 함수에서 값전달의 추가비용
- 전달되는 타입, 왼값 인수 대 오른값 인수 비율, 타입이 동적 메모리 할당을 사용하는지의 여부에 의존
- 동적 메모리 할당을 사용한다면, 그 타입의 대정 연산자의 구현 방식과 대입 대상에 연관된 메모리가 대입의 원본에 연관된 메모리만큼 큰지의 여부에도 의존
- std::string의 경우 SSO 최적화가 수행되는지에도 의존
값전달 함수들이 꼬리를 물면, 호출 연쇄의 비용은 커짐
- 참조전달에서는 그런 비용이 누적되지 않음
잘림 문제(slicing problem)
- 함수가 기반 클래스 타입이나 그로부터 파생된 임의의 타입의 매개변수를 받는 경우에는 그 매개변수를 값전달 방식으로 선언하지 않는 것이 좋음
- 파생 클래스 부분이 잘려나가기 때문
기억해 둘 사항들
이동이 저렴하고 항상 저장되는 복사 가능 매개변수에 대해서는 값 전달이 참조 전달만큼 효율적, 구현 쉽고, 산출되는 목적 코드의 크기도 작음
왼값 인수의 경우 값 전달(복사생성) 다음의 이동 대입은 참조 전달 다음의 복사 대입보다 비용이 비쌀 가능성이 있음
값 전달에서 잘림문제 발생 가능 (상속)
항목42: 삽입 대신 생성 삽입을 고려하라
- std::vector 등 컨테이너의 삽입함수
- insert, push_back, push_front; (std::forward_list의 insert_after)
1
2
std::vector<std::string> vs;
vs.push_back("hello");
- 위와 같이 오른값으로 넘겨주는 경우
1
2
3
4
5
6
7
8
9
template <class T, // from the C++11
class Allocator = allocator<T>> // Standard
class vector {
public:
…
void push_back(const T& x); // insert lvalue
void push_back(T&& x); // insert rvalue
…
};
- 컴파일러는 위 상황에서 인수의 타입(
const char[6]
)과push_back
이 받는 매개변수의 타입이 일치하지 않음을 인식함- 타입 불일치를 해소하기 위해 컴파일러는 아래와 같은 코드를 생성
1
vs.push_back(std::string("xxxx")); // 임시 std::string 객체를 새성해서 push_back에 전달
- 위 코드는 생성자를 두번 호출하며, 소멸자도 실행하기 때문에 효율성이 나쁨
1
vs.emplace_back(10, 'x');
emplace 함수는 완벽전달을 하므로, 위처럼 vs안에서 바로 생성할 수 있음
- 표준 컨테이너 (, std::array 제외)
- emplace 지원
- std::forwrd_list 는 emplace_after
- 연관 컨테이너
- insert에 대응되는 emplace_hint
생성 emplacement
성능이 더 유연 (생성과 파괴의 비용이 없음)
- 생성자를 위한 인수들을 받기 때문
삽입함수가 더 빠르게 실행되는 상황
- 생성 삽입이 요청된 곳, 컨테이너가 담는 타입의 생성자의 예외안전성, 값 붕복이 금지된 연관 컨테이너의 경우 추가할 값이 컨테이너에 이미 있는지의 여부 등 다양한 요인이 영향을 미치기 때문에 직접 측정해봐야함
아래 3조건을 모두 성립한다면 거의 항상 생성 삽입의 성능이 더 좋음
추가할 값이 컨테이너에 대입되는 것이 아니라 컨테이너 안에서 생성해야함
- 이미 다른 객체가 차지하고 있는 위치에 배치하는 것은 비효율적
- 이동 대입 하기 위해, 이동의 대상인 임시 객체를 생성함
- 노드기반 컨테이너들은 거의 항상 생성을 통해 새 값을 추가 (ㅍector, deque, string, array 제외한 컨테이너들)
- 이미 다른 객체가 차지하고 있는 위치에 배치하는 것은 비효율적
추가할 인수 타입들이 컨테이너가 담는 타입과 달라야함 (생성자를 위한 인수들을 받아야함)
- 어떤 컨테이너
에 T 타입의 객체를 추가할 때에는 생성 삽입이 삽입보다 빠를 이유가 없음 - 삽입 인터페이스에서도 임시 객체를 생성할 필요가 없기 때문
- 어떤 컨테이너
컨테이너가 기존 값과의 중복 때문에 새 값을 거부할 우려가 별로 없어야함
- 노드 컨테이너의 생성 삽입 구현은 보통, 새 값으로 노드를 생성해서 그것을 기존 컨테이너 노드들과 비교하기 때문
- 만약 없으면 노드를 연결함
- 값이 이미 있으면, 삽입 생성 취소, 그 노드가 파괴 (생성과 파괴 비용이 들어감)
언제 사용?
자원관리와 관련된 것
1
2
3
4
5
6
std::list<std::shared_ptr<Widget>> ptrs;
void killWidget(Widget* pWidget);
// 어떤 방식이든, push_back호출 전 임시 std::shared_ptr 객체가 생성됨
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
ptrs.push_back({ new Widget, killWidget });
- 위 컨테이너에 커스텀 삭제자가 있는 shared_ptr를 추가한다고 가정하면, 이 경우 반드시 new 로 생성해야함
- 임시 shared_ptr 이 생성됨 (push_back의 매개변수는 참조이므로, 참조할 객체가 반드시 있어야함)
1
ptrs.emplace_back(new Widget, killWidget);
- emplace_back을 사용하면 메모리 누수 가능성있음
push_back은 예외가 발생해도 어떠한 누수도 일어나지 않음
- 임시 객체 생성(new)
- 임시객체를 push_back에서 참조로 받음. 복사본을 담을 목록 노드를 할당하는 도중 메모리 부족 예외
- 예외가 전파, 임시객체 파괴, 메모리 해제
emplace_back은 메모리 누수가 발생
- raw 포인터가 emplace_back으로 완벽전달, emplace_back은 새 값을 담을 리스트 노드를 할당, 할당이 실패하고 메모리 부족 예외 발생
- 예외가 emplace_back 밖으로전파, 힙에 있는 Widget 객체에 도달하는 유일한 수단인 raw pointer가 사라짐 (자원 누수)
emplace는 자원 관리 객체의 생성이 컨테이너의 메모리 안에서 그것을 생성할 수 있는 시점까지 지연됨
- 그 시점까지 예외가 발생하면, 자원 누수가 일어남
아래는 예외 안전한 코드
- 그러나 생성과 파괴 비용이 들어감
1
2
3
4
std::shared_ptr<Widget> spw1(new Widget, killWidget);
ptr.push_back(std::move(spw1));
std::shared_ptr<Widget> spw2(new Widget, killWidget);
ptr.emplace_back(std::move(spw2));
explicit과 emplace
C++11에서 추가된 std::regex 생성자는 explicit로 선언되어 있음
std::regex r = nullptr;
// 컴파일 에러regexs.(nullptr);
// 컴파일 에러- 포인터에서 regex로의 암묵적 변환을 요청하지만, explicit라서 그러한 변환을 거절
emplace_back 호출시에는 지정한 nullptr는 std::regex 객체로 변환할 무엇이 아닌, 생성자에 전달할 인수
- 컴파일러는 이를 암묵적 변환 요청으로 간주하지 않고, 다음 코드를 작성한 것 처럼 취급
- std::regex r(nullptr) // 미정의 행동
explicit 생성자에서는 복사 초기화를 사용 못하지만, 직접 초기화는 사용 가능하기 때문에 생기는 문제
- std::regex r1 = nullptr // 오류: 컴파일 에러
- 복사 초기화 (copy initialization)
- std::regex r2(nullptr) // 컴파일됨
- 괄호, 중괄호가 있는것은 직접 초기화
- std::regex r1 = nullptr // 오류: 컴파일 에러
- push_back: 복사 초기화
- emplice_back: 직접 초기화
기억해 둘 사항들
- 이론적으로, 생성 삽입 함수들은 종종 해당 삽입 버전보다 더 효율적이어야하며, 덜 효율적인 경우는 절대로 없어야함
- 추가하는 값이 컨테이너로 대입되는 것이 아니라 생성되고, 인수들이 컨테이너가 담는 타입과 다르고, 그 값이 중복값 체크를 하지 않는 다면,
- emplace_back(생성 삽입)이 push_back(삽입)보다 더 효율적일 수 있음
- 생성 삽입 함수를 사용할 때 제대로 된 인수를 넘겨주자
- 삽입함수였더라면, 거부당했을 타입 변환들을 수행 가능