readme.md
기록소
readme.md
전체 방문자
오늘
어제
  • 분류 전체보기
    • 네트워크
      • HTTP
      • 윈도우 소켓 프로그래밍
    • Windows API
    • 그래픽스
      • DirectX11
    • 일반
      • Linux
      • 데이터베이스
      • 팁
      • 책 후기
    • 쿠버네티스
    • 프로그래밍 언어
      • C#
      • Java
      • Go
      • C++
      • Lua
    • 책
      • 이펙티브 C++
      • 제프리 리처의 WINDOWS VIA C, C++
    • 기타

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 대입연산자
  • const
  • directx11
  • imagestride
  • Graphics
  • 초기화
  • phong
  • 캐스팅
  • id3d11shaderresourceview
  • new
  • 버텍스 버퍼
  • 설계
  • wm_keyup
  • 자원관리
  • consteval
  • DirectX
  • C++
  • windowsAPI
  • 생성자
  • 인터페이스
  • Delete
  • 가상함수
  • emplace
  • 상속
  • 자바8
  • 윈도우 소켓
  • 템플릿
  • 소멸자
  • 소켓 프로그래밍
  • CPP

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
readme.md

기록소

책/이펙티브 C++

이펙티브 C++(340p ~ 350p)

2024. 2. 10. 11:12

이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 340p ~ 350p

이펙티브 C++(340p ~ 350p)

요약

항목 48: 템플릿 메타프로그래밍, 하지 않겠는가?

  • 템플릿 메타프로그래밍은 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 일을 말한다.
  • 템플릿 메타프로그램은 C++ 컴파일러가 실행시키는 C++로 만들어진 프로그램이다.
  • TMP 프로그램이 실행을 마친 후에 결과로 나온 출력물이 일반 컴파일 과정을 거친다.
  • TMP는 다음과 같은 강점을 가진다.
    • TMP를 쓰면 다른 방법으로는 까다롭거나 불가능한 일을 쉽게 할 수 있다.
    • 템플릿 메타프로그램은 컴파일이 진행되는 동안 실행되기 때문에, 기존 작업을 런타임 영역에서 컴파일 영역으로 전환할 수 있다.
// iter를 d 단위만큼 이동
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    if (iter가 임의 접근 반복자이다)
    {
        iter += d;
    }
    else {
        if (d >= 0) { while (d--) ++iter; }
        else { while (d--) --iter; }
    }
}
  • 이 유사코드를 진짜 코드로 만들려면 typeid를 사용할 수 있다. typeid를 사용한다는 것은 타입 정보를 꺼내는 작업을 런타임에 하겠다는 것과 같다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    if (typeid(typename std::iterator_traits<IterT>::iterator_category)
        == typeid(random_access_iterator_tag))
    {
        iter += d;
    }
    else {
        // ...
    }
}
  • typeid 연산자를 쓰는 방법은 특성정보를 쓰는 방법보다 효율이 떨어진다.
    • 타입 점검 동작이 컴파일 도중이 아니라 런타임에 일어난다.
    • 런타임 타입 점검을 수행하는 코드는 어쩔 수 없이 실행 파일에 들어가야 한다.
  • 따라서 위의 코드는 TMP가 보통의 C++ 프로그램보다 효율이 좋은지 보여주는 단적인 예라고 할 수 있다.
  • TMP를 사용해서 if .. else 구문의 처리를 컴파일 타임에 할 수 있게 된다.
  • typeid 방법은 성능 외에도 컴파일 문제를 일으킬 수 있다.
std::list<int>::iterator iter;

// iter를 10개만큼 앞으로 옮기고 싶었으나, 컴파일 에러가 발생
advance(iter, 10);
  • 위의 코드를 컴파일러가 돌린다고 가정했을 때, 어떤 advance가 만들어질지 생각해보자. 템플릿 매개변수인 IterT와 DistT에 대해 iter의 타입과 10의 타입을 넣으면 다음과 같은 advance가 생길 것이다.
void advance(std::list<int>::iterator& iter, int d)
{
    if (typeid(typename std::iterator_traits<IterT>::iterator_category)
        == typeid(random_access_iterator_tag))
    {
        iter += d;            // 에러
    }
    else {
        if (d >= 0) { while (d--) ++iter; }
        else { while (d--) --iter; }
    }
}
  • 에러가 발생한 원인은 += 연산자를 사용한 부분 때문이다. list의 iterator는 양방향 반복자이기 때문에 += 연산자를 지원하지 않는다. += 연산은 임의 접근 반복자만 가능하다.
  • 실제로는 if 문에서 typeid 점검이 실패하기 때문에 += 연산자가 호출되는 라인까지 실행되지도 않지만, 컴파일러는 모든 소스 코드가 제대로 되어 있는지 확인하는 책임이 있기 때문에 에러를 발생시킨다.
  • TMP에는 반복 의미의 진정한 루프는 없기 때문에 재귀를 사용해서 루프의 효과를 낸다.
    • 그런데 이 재귀도 우리가 알고 있는 종류가 아니라 재귀식 템플릿 인스턴스화를 통해 구현한다.
  • TMP에서 처음 접하는 프로그램은 컴파일을 통해 팩토리얼을 계산하는 템플릿이다.
    • TMP의 팩토리얼 계산에서 재귀식 템플릿 인스턴스화를 통한 루프 효과를 확인할 수 있다.
    • TMP에서 변수를 만들어서 사용하는 방법도 엿볼 수 있다.
// 일반적인 경우
template<unsigned n>
struct Factorial {
    enum { value = n * Factorial<n-1>::value };
};

// 특수한 경우
template<>
struct Factorial<0> {
    enum { value = 1 };
};
  • 이렇게 만들어진 템플릿 메타프로그램이 있으면 Factorial<n>::value를 참조함으로써 n의 계승을 바로 얻을 수 있다.
  • 이 코드에서 루프를 도는 위치는 템플릿 인스턴스인 Factorial<n>의 내부에서 또 다른 템플릿 인스턴스인 Factorial<n-1>을 참조하는 곳이다.
  • 제대로 만들어진 재귀 코드가 그러하듯, 재귀를 끝내는 특수 조건이 있어야 한다. Factorial<0>
  • Factorial 템플릿은 구조체 타입이 인스턴스화되도록 만들어져 있다. 이렇게 만들어진 구조체 안에는 value라는 이름의 TMP 변수가 선언되어 있는데, enum hack을 사용했다.
  • TMP는 루프 대신에 재귀식 템플릿 인스턴스화를 사용하기 때문에, 꼬리에 꼬리를 물고 만들어지는 템플릿 인스턴스화 버전마다 자체적으로 value의 사본을 가지게 되고, 각각의 value는 루// 프를 한 번 돌 때마다 만들어지는 값이 저장된다.
// 120을 런타임 계산 없이 출력
std::cout << Factorial<5>::value;

// 3628800을 런타임 계산 없이 출력
std::cout << Factorial<10>::value;
  • C++ 프로그래밍에서 TMP가 사용될 수 있을 부분은 다음과 같다.

치수 단위의 정확성 확인

  • 과학 기술 분야의 응용프로그램을 만들 때는 무엇보다도 치수 단위가 똑바로 조합되어야 한다.
  • TMP를 사용하면 프로그램 안에서 쓰이는 모든 치수 단위의 조합이 제대로 됐는지를 맞춰 컴파일 동안에 볼 수 있다.
  • TMP를 사용하면 단위 조합이 제대로 맞춰줬는지 컴파일 시간에 확인이 가능하기 때문에 런타임에 이러한 조합을 검증하는 로직이 필요하지 않다.
  • TMP는 분수식 지수 표현이 지원된다. 이를 위해 컴파일 도중 분수의 약분이 되어야 한다.

행렬 연산의 최적화

  • operator* 등의 어떤 연산자 함수는 연산 결과를 새로운 객체에 담아 반환해야 한다.
  • 항목 44에서 만든 SquareMatrix 클래스를 이용해서 다음과 같은 행렬 연산을 하는 코드를 작성했다.
typedef SquareMatrix<double, 10000> BigMatrix;
BigMatrix m1, m2, m3, m4, m5;

BigMatrix result = m1 * m2 * m3 * m4 * m5;
  • 곱셈 결과를 보통 방법으로 계산하려면 네 개의 임시 행렬이 생겨야 한다.
  • operator*를 한 번씩 호출할 때마다 반환되는 결과로 생기는 것이다.
  • 행렬 원소들 사이의 곱셈을 해야 하므로 네 개의 루프가 순차적으로 만들어질 수 밖에 없다.
  • 이런 비싼 연산에 TMP를 사용할 수 있다. TMP를 응용한 표현식 템플릿을 사용하면 덩치 큰 임시 객체를 없애는 것은 물론이고 루프까지 합쳐 버릴 수 있다.

맞춤식 디자인 패턴 구현의 생성

  • 전략 패턴, 감시자 패턴, 방문자 패턴 등의 디자인 패턴은 그 구현 방법이 여러 가지일 수 있다.

  • TMP를 사용한 프로그래밍 기술인 정책 기반 설계라는 것을 사용하면 따로따로 마련된 설계상의 선택을 나타내는 템플릿을 만들어낼 수 있다.

  • 이렇게 되면 만들어진 정책 템플릿은 서로 임의대로 조합되어 사용자의 취향에 맞는 동작을 갖는 패턴으로 구현되는 데 쓰인다.

    • 몇 개의 스마트 포인터 동작 정책을 하나씩 구현한 각각의 템플릿을 만들어 놓고, 이들의 사용자가 마음대로 조합하여 수백 가지의 스마트 포인터 타입을 생성할 수 있게(컴파일 도중에) 하는 것이다.
  • 생성싱 프로그래밍의 기초가 바로 이 기술이다.

  • TMP는 누구나 쉽고 재미있게 할 수 있는 프로그래밍은 아니다. 문법은 비직관적이고, 개발도구의 지원도 미약하다.

  • 그러나 기존 작업을 런타임에서 컴파일 타임으로 전환함으로써 얻을 수 있는 효율 향상은 포기하기 쉽지 않다.

8. new와 delete를 내 맘대로

  • C++은 수동으로 메모리를 관리하는데, 자신들이 만들 소프트웨어의 메모리 사용 성향을 연구한 후에, 그 연구 결과에 맞추어 메모리 할당 루틴과 해제 루틴을 다듬음으로써 가능한 최대의 수행 성능을 제공할 수 있다.
  • 개발자로서 이런 작업이 가능하려면 일단 C++의 메모리 관리 루틴이 어떻게 동작하는지를 면밀히 파악해야 한다.
  • 다중 스레드 환경에서의 메모리 관리는 단일 스레드 시스템에서는 경험할 수 없는 화끈한 맛을 느끼게 하는 여러 가지 문젯거리를 안고 있다.
    • 힙은 수정 가능한 전역 자원으로 분류된다.
    • 다중 스레드 시스템에서는 스레드들이 이런 전역 자원에 미친 듯이 접근하면서 경쟁 상태가 생길 소지가 득실득실해진다고 생각하면 된다.
    • 이 부분에 적절한 동기화를 걸지 않으면 아무런 소용이 없다.

항목 49: new 처리자의 동작 원리를 제대로 이해하자

  • 사용자가 보낸 메모리 할당 요청을 operator new 함수가 맞추어 주지 못할 경우에(메모리 할당이 실패하는 경우) operator new 함수는 예외를 던지게 되어 있다.
    • 옛날에는 널 포인터를 반환했다.
  • 메모리 할당이 제대로 되기 못한 상황에 대한 반응으로 operator new가 예외를 던지기 전에, 이 함수는 사용자 쪽에서 지정할 수 있는 에러 처리 함수를 우선적으로 호출하도록 되어 있다.
  • 이 에러 처리 함수를 가리켜 new 처리자(new-handler)라고 한다.
  • 이와 같은 메모리 고갈 상황을 처리할 함수를 사용자 쪽에서 지정할 수 있도록, 표준 라이브러리에는 set_new_handler라는 함수가 준비되어 있다. 이 함수는 <new>에 선언되어 있다.
namespace std {
    typedef void (*new_handler) ();
    new_handler set_new_handler(new_handler p) throw();
}
  • new_handler는 받는 것도 없고 반환하는 것도 없는 함수 포인터에 대해 typedef를 걸어 놓은 타입동의어이다.
  • set_new_handler는 new_handler를 받고 new_handler를 반환하는 함수이다.
    • throw는 예외 지정이라고 불리는 부분이다.
    • 이 함수는 어떤 예외도 던지지 않을 것이라는 뜻이다.
  • set_new_handler가 받아들이는 new_handler 타입의 매개변수는 요구된 메모리를 operator new가 할당하지 못했을 때 operator new가 호출할 함수의 포인터이다. 반환 값은 지금의 set_new_handler가 호출되기 바로 전까지 new 처리자로 쓰이고 있던 함수의 포인터이다.
// 충분한 메모리를 operator new가 할당하지 못했을 때 호출할 함수
void outOfMem()
{
    std::cerr << "Unable to satisfy request for memory\n";
    std::abort();
}

int main()
{
    std::set_new_handler(outOfMem);

    int* pBigDataArray = new int[10000000000L];
}
  • 만약 operator new가 1억 개의 정수 할당에 실패하면 outOfMem 함수가 호출될 것이고, 이 함수는 에러 메시지를 출력하면서 프로그램을 강제로 끝내 버릴 것이다.
  • 사용자가 부탁한 만큼의 메모리를 할당해 주지 못하면, operator new는 충분한 메모리를 찾아낼 때까지 new 처리자를 되풀이해서 호출한다.
    • new 처리자를 반복 호출하는 코드는 항목 51에서 자세히 살펴보겠지만, 응용프로그램 개발자 입장에서는 굳이 이 부분까지 내려갈 필요는 없다.
  • 어쨌든 이를 통해 호출되는 new 처리자 함수가 프로그램의 동작에 좋은 영향을 미치도록 설계되어 있다면 다음 동작 중 하나를 꼭 해주어야 한다.

사용할 수 있는 메모리를 더 많이 확보한다.

  • operator new가 시도하는 이후의 메모리 확보가 성공할 수 있도록 하자는 전략이다.
  • 구현 방법은 여러 가지가 있지만, 프로그램이 시작할 때 메모리 블록을 크게 하나 할당해 놓았다가 new 처리자가 가장 처음 호출될 때 그 메모리를 쓸 수 있도록 허용하는 방법이 그 한 가지이다.

다른 new 처리자를 설치한다.

  • 현재의 new 처리자가 더 이상 가용 메모리를 확보할 수 없다 해도, 이 경우에 자기 몫까지 해 줄 다른 new 처리자의 존재를 알고 있을 가능성도 있다.
  • 만약 그렇다면 현재의 new 처리자는 제자리에서 다른 new 처리자를 설치할 수 있다.
    • 현재의 new 처리자 안에서 set_new_handler를 호출하는 것이다.
  • operator new가 다시 new 처리자를 호출할 때가 되면, 새로 설치된 new 처리자가 호출된다.
  • 이렇게 만드는 한 가지 방법은 new 처리자의 동작을 조정하는 데이터를 정적 데이터 혹은 네임스페이스 유효범위 안의 데이터, 아니면 전역 데이터로 마련해 둔 후에 new 처리자가 이 데이터를 수정하게 만드는 것이다.

new 처리자의 설치를 제거한다.

  • 다시 말해, set_new_handler에 널 포인터를 넘긴다.
  • new 처리자가 설치된 것이 없으면, operator new는 메모리 할당이 실패했을 때 예외를 던진다.

예외를 던진다.

  • bac_alloc 혹은 bad_alloc에서 파생된 타입의 예외를 던진다.
  • operator new에는 이쪽 종류의 에러를 받아서 처리하는 부분이 없기 때문에, 이 예외는 메모리 할당을 요청한 원래의 위치로 전파(propagate)된다.

복귀하지 않는다.

  • 대개 abort 혹은 exit을 호출한다.

메모

항목 48: 템플릿 메타프로그래밍, 하지 않겠는가?

  • 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 낸다. 따라서 TMP를 쓰면 선행 에러 탐지와 높은 런타임 효율을 손에 거머쥘 수 있다.
  • TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 또한 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있다.

'책 > 이펙티브 C++' 카테고리의 다른 글

이펙티브 C++(360p ~ 369p)  (0) 2024.02.13
이펙티브 C++(350p ~ 361p)  (1) 2024.02.12
이펙티브 C++(332p ~ 340p)  (1) 2024.02.09
이펙티브 C++(312p ~ 320p)  (1) 2024.02.08
이펙티브 C++(320p ~ 331p)  (1) 2024.02.08
    '책/이펙티브 C++' 카테고리의 다른 글
    • 이펙티브 C++(360p ~ 369p)
    • 이펙티브 C++(350p ~ 361p)
    • 이펙티브 C++(332p ~ 340p)
    • 이펙티브 C++(312p ~ 320p)
    readme.md
    readme.md

    티스토리툴바