본문 바로가기
C++/Effective C++

Effective C++ (46) ~ (50)

by 계발자 망고 2021. 8. 28.

46) 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자.

항목 24에서 모든 매개변수에 대해서 암시적 타입 변환이 되도록 만들기 위해서는 비멤버 함수밖에 방법이 없다는 이야기를 했었다. 이것을 템플릿으로 확장해보자.

template<typename T>
class Rational {
public:
  Rational(const T& numerator = 0,
           const T& denominator = 1); // 매개변수가 참조자로 전달되는 이유는 항목 20

  const T numerator() const;
  const T denominator() const;
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
                            const Rational<T>& rhs)
{ ... }

혼합형 수치 연산이 필요하기에, 위와같은 코드를 짰으나 활용을 위해 다음과 같은 코드를 쓰면 컴파일이 안된다.

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 에러

컴파일러 입장에서는 Rational<T> 타입을 두개 받아들이는 operator*를 인스턴스화 하고 싶어도 방법이 없다.

예제의 두번째 파라미터인 2와 같은 경우 컴파일러가 Rational<int>라고 유추하지 못한다.

이유는 템플릿 인자 추론에서는 암시적 타입 변환이 고려되지 않기 때문이다.

이때 해결책은 클래스 템플릿 안에 프렌드 함수를 선언하는 것이다.

template<typename T>
class Rational{
public:
  ...
  friend const Rational operator*(const Rational& lhs, const Rational& rhs);
  ...
};

이제 Rational<int>클래스가 템플릿의 인스턴스로 만들어지고, 그 과정의 일부로 Rational<int> 타입의 매개변수를 받는 프렌드 함수인 operator*가 자동으로 선언된다.

이제 그 다음엔 이전과 달리 함수 템플릿이 아닌 함수가 선언된 것이므로, 컴파일러가 호출문에서 암시적 변환 함수를 적용할 수 있게 된다. 따라서 컴파일에 성공한다.

이때 알아둬야 할 점은 Rational<T>안에서는 Rational이라고만 써도 컴파일러에게 Rational<T>로 인식된다.

 

그러나 지금까지의 코드로 함수 링크는 되지 않는다.

이 함수가 Rational 안에서 선언만 되어있지 정의가 되어있지 않기 때문이다.

따라서 아래와 같이 선언부를 덧붙인다

template<typename T>
class Rational{
public:
  friend const Rational operator*(const Rational& lhs, const Rational& rhs)
  {
    return Rational(lhs.numerator() * rhs.numerator(),
                    lhs.denominator() * rhs.denominator());
  }
};

항목 30에서 나왔던것과 같이 클래스 안에 정의된 함수는 암시적으로 인라인으로 선언된다. 지금의 operator도 마찬가지이다. 지금은 간단한 함수라 괜찮지만, 복잡한 함수에서는 클래스 바깥에서 정의된 도우미 함수만 호출하는 식으로 구현해서 이 영향을 최소화할 수 있다.

template<typename T> class Rational;

template<typename T> // 도우미 함수 템플릿
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);

template<typename T>
class Rational{
public:
  friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
  { return doMultiply(lhs, rhs); }
};

template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
  return Rational<T>(lhs.numerator() * rhs.numerator(),
                     lhs.denominator() * rhs.denominator());
}

47) 타입에 대한 정보가 필요하면 특성정보 클래스를 이용하자

컴파일 도중에 주어진 타입의 정보를 얻을 수 있도록 하는 객체를 특성정보(traits) 클래스라고 한다.

https://accu.org/journals/overload/9/43/frogley_442/

 

An introduction to C++ Traits

It is not uncommon to see different pieces of code that have basically the same structure, but contain variation in the details. Ideally we would be able to reuse the structure, and factor out the variations. In 'C' this might be done by using function poi

accu.org

특성 정보 클래스를 설계할 때에는

  • 다른사람이 사용하도록 열어주고 싶은 타입 관련 정보를 확인한다 (반복자라면 반복자 범주 등)
  • 그 정보를 식별하기 위한 이름을 선택한다(ex: iterator_category)
  • 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전(ex: iterator_traits)을 제공한다

https://en.cppreference.com/w/cpp/types

48) 템플릿 메타 프로그래밍

템플릿 메타 프로그래밍(TMP)이란 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 일을 말한다.

여러 강점이 있다.

  • TMP를 쓰면 다른 방법으로는 까다롭거나 불가능한 일을 쉽게 할 수 있다.
  • 컴파일이 진행되는 동안 실행되기 때문에 작업을 런타임에서 컴파일 타임으로 가져올 수 있다.
  • 실행 코드가 작아지고 런타임도 짧아지고 메모리도 적게 먹는다.

다음은 팩토리얼 연산의 TMP 예시이다.

template<unsigned n>
struct Factorial {
  enum { value = n * Factorial<n-1>::value };
};

template<>
struct Factorial<0>{
  enum { value = 1 };
};

// 사용 예시
std::cout << Factorial<5>::value;

TMP를 활용하기 좋은 곳은 다음과 같다.

  • 치수 단위(dimensional unit)의 정확성 확인
  • 행렬 연산의 최적화
  • 맞춤식 디자인 패턴 구현의 생성

그러나 문법이 직관적이지 않아 난이도가 높고 개발 도구의 지원도 많지 않다.

8. New와 Delete를 내 맘대로

49) 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를 걸어놓은 타입 동의어다.

void outOfMem()
{
  std::cerr << "Unable to satisfy request for memory\n";
  std::abort();
}

int main()
{
  std::set_new_handler(outOfMem);
  int *pBigDataArray = new int[1000000000L];
}

좋은 new 처리자는 다음 동작 중 하나를 꼭 해주어야 한다.

  • 사용할 수 있는 메모리를 더 많이 확보한다.
  • 다른 new 처리자를 설치한다.
  • new 처리자의 설치를 제거한다.
  • 예외를 던진다
  • 복귀하지 않는다

특정 클래스에 대한 자체 set_new_handler와 operator new를 구현하는 것도 가능하다

// Widget.h
class Widget{
public:
  static std::new_handler set_new_handler(std::new_handler p) throw();
  static void * operator new(std::size_t size) throw(std::bad_alloc);
  
private:
  static std::new_handler currentHandler;
}

// Widget.cpp
std::new_handler Widget::currentHandler = 0;

std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
  std::new_handler oldHandler = currentHandler;
  currentHandler = p;
  return oldHandler;
}

이제 Widget의 operator new가 할 일만 남았다.

  • 표준 set_new_handler 함수에 Widget의 new 처리자를 넘겨서 호출한다. 즉, 전역 new 처리자로서 Widget의 new 처리자를 설치한다.
  • 전역 operator new 를 호출하여 실제 메모리 할당을 수행한다.
  • 전역 operator new가 할당한 메모리를 반환함과 동시에 전역 new 처리자를 관리하는 객체의 소멸자가 호출되면서 Widget의 operator new가 호출되기 전에 쓰이고 있던 전역 new 처리자가 자동으로 복원된다.
class NewHandlerHolder{
public:
  explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {} // new 처리자 획득
  ~NewHandlerHolder() { std::set_new_handler(handler); } // 해제
  
private:
  std::new_handler handler;
  
  // 복사 방지 (항목 14)
  NewHandlerHolder(const NewHandlerHolder&);
  NewHandlerHolder& operator=(const NewHandlerHolder&); 
};

void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
  // Widget의 new 처리자 설치
  NewHandlerHolder h(std::set_new_hander(currentHandler));
  
  return ::operator new(size);
}

// 사용 예시
void outOfMem();

Widget::set_new_handler(outOfMem); // Widget만을 위한 new 처리자 설치

Widget *pw1 = new Widget; // 메모리 할당 실패시 outOfMem 호출됨
std::string *ps = new std::string; // 메모리 할당 실패시 전역 new 처리자(있으면) 호출
Widget::set_new_handler(0); // Widget 클래스만을 위한 new처리자를 null로 설정
Widget *pw2 = new Widget; // 이때는 메모리 할당 실패시 바로 예외

50) new 및 delete를 언제 바꿔야 할까

기본적으로 제공되는 operator new와 operator delete를 재정의하는 이유는 다음과 같다.

  • 잘못된 힙 사용을 탐지하기 위해
  • 할당 및 해제 속력을 높이기 위해
  • 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
  • 기본 메모리 관리자의 공간 오버헤드를 줄이기 위해
  • 적당히 타협한 기본 할당자의 바이트 정렬 동작을 보장하기 위해
  • 임의의 관계를 맺고있는 객체들을 한군데에 나란히 모아놓기 위해
  • 그때그때 원하는 동작을 수행하도록 하기 위해

 

반응형

'C++ > Effective C++' 카테고리의 다른 글

Effective C++ (51) ~ (55)  (0) 2021.08.29
Effective C++ (41) ~ (45)  (0) 2021.08.26
Effective C++ (36) ~ (40)  (0) 2021.08.25
Effective C++ (31) ~ (35)  (0) 2021.08.24
Effective C++ (26) ~ (30)  (0) 2021.08.22

댓글