Home [C++] emc++ 07: Tweaks
Post
Cancel

[C++] emc++ 07: Tweaks

  • 값전달 기법, 생성 삽입 기능을 고려하여라

항목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을 이동
      • 복사본으로 완전히 독립적인 객체
      • 마지막 사용
  • C++98에서는 값 전달의 비용이 컸음
    • C++98에서는 호출자가 무엇을 넘겨주든, 매개변수 newName이 복사 생성에 의해 생성
  • C++11에서는 newName은 인수가 왼값일 때만 복사 생성에 의해 생성, 오른값일 때는 이동 생성에 의해 생성

접근방식 3가지 효율성

  1. 왼값, 오른값 중복적재 (참조 전달)

    • 복사, 이동 연산을 기준으로 한 비용은 없음
    • 왼값: 복사1회
    • 오른값: 이동 1회
  2. 보편참조 (참조 전달)

    • 참조이기에 비용없는 연산
    • 왼값: 복사 1사
    • 오른값: 이동 1회
    • 만약 생성자의 파라미터로 들어가는 값 (std::string 과 literal string)이면 복사 or 이동 연산이 0회 이상
  3. 값전달

    • 매개변수가 반드시 생성됨
    • 왼값: 복사 생성 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
};
  1. 생성을 통한 복사

    • Pssword()
      • 값전달을 사용하면 다른 대안들에 비해 이동이 한 번 더 수행
  2. = 연산자를 통한 복사(복사 연산자, 이동 연산자)

    • 대입 기반 매개변수 복사의 비용은 대입에 관여하는 객체의 값에 의존(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조건을 모두 성립한다면 거의 항상 생성 삽입의 성능이 더 좋음

  1. 추가할 값이 컨테이너에 대입되는 것이 아니라 컨테이너 안에서 생성해야함

    • 이미 다른 객체가 차지하고 있는 위치에 배치하는 것은 비효율적
      • 이동 대입 하기 위해, 이동의 대상인 임시 객체를 생성함
    • 노드기반 컨테이너들은 거의 항상 생성을 통해 새 값을 추가 (ㅍector, deque, string, array 제외한 컨테이너들)
  2. 추가할 인수 타입들이 컨테이너가 담는 타입과 달라야함 (생성자를 위한 인수들을 받아야함)

    • 어떤 컨테이너에 T 타입의 객체를 추가할 때에는 생성 삽입이 삽입보다 빠를 이유가 없음
      • 삽입 인터페이스에서도 임시 객체를 생성할 필요가 없기 때문
  3. 컨테이너가 기존 값과의 중복 때문에 새 값을 거부할 우려가 별로 없어야함

    • 노드 컨테이너의 생성 삽입 구현은 보통, 새 값으로 노드를 생성해서 그것을 기존 컨테이너 노드들과 비교하기 때문
    • 만약 없으면 노드를 연결함
    • 값이 이미 있으면, 삽입 생성 취소, 그 노드가 파괴 (생성과 파괴 비용이 들어감)

언제 사용?

자원관리와 관련된 것

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은 예외가 발생해도 어떠한 누수도 일어나지 않음

  1. 임시 객체 생성(new)
  2. 임시객체를 push_back에서 참조로 받음. 복사본을 담을 목록 노드를 할당하는 도중 메모리 부족 예외
  3. 예외가 전파, 임시객체 파괴, 메모리 해제

emplace_back은 메모리 누수가 발생

  1. raw 포인터가 emplace_back으로 완벽전달, emplace_back은 새 값을 담을 리스트 노드를 할당, 할당이 실패하고 메모리 부족 예외 발생
  2. 예외가 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) // 컴파일됨
      • 괄호, 중괄호가 있는것은 직접 초기화
  • push_back: 복사 초기화
  • emplice_back: 직접 초기화

기억해 둘 사항들

  • 이론적으로, 생성 삽입 함수들은 종종 해당 삽입 버전보다 더 효율적이어야하며, 덜 효율적인 경우는 절대로 없어야함
  • 추가하는 값이 컨테이너로 대입되는 것이 아니라 생성되고, 인수들이 컨테이너가 담는 타입과 다르고, 그 값이 중복값 체크를 하지 않는 다면,
    • emplace_back(생성 삽입)이 push_back(삽입)보다 더 효율적일 수 있음
  • 생성 삽입 함수를 사용할 때 제대로 된 인수를 넘겨주자
    • 삽입함수였더라면, 거부당했을 타입 변환들을 수행 가능
This post is licensed under CC BY 4.0 by the author.

[C++] emc++ 06: Thread

-