z 'C++공부/C++ Templates' 카테고리의 글 목록 :: C++, 그래픽 기술 블로그

개념이란 하나나 그 이상의 파라미터에 대한 제약 조건에 대해 이름을 붙인 것입니다.

E.1 개념 사용

template<typename T> requires LessThanComparable<T>
T max(T a, T b) {
  return b < a? a: b;
};

requires절을 통해 뒤의 불리언 조건자로 상수 표현식에 평가되는 LessThanComparable을 사용하여 이 결과가 참일 경우에만 인스턴스화되도록 합니다. 요구 절을 개념으로 표현하지 않아도 되고 어떠한 불리언 상수 표현식이라도 됩니다. 이는 && 혹은 ||을 통해 여러 조건을 조합할 수도 있습니다. 또한 다음과 같이 제약 조건을 약식으로 사용하는 것도 가능합니다.

template<LessThanComparable T>
T max(T a, T b) {
  return b < a? a: b;
};

E.2 개념 정의

개념은 bool 형식의 constexpr 변수 템플릿과 거의 같지만 형식이 명시되지는 않습니다.

template<typename T>
concept LessThanComparable = requires(T x, T y) {
  { x < y } -> bool
};

요구 표현식에서 부가적인 파라미터 목록을 더 받을 수도 있으며, 요구 사항 {x < y} -> bool은 안의 조건이 SIFNAE 법칙에 유효해야만 하며 그 결과가 bool로 표현될 수 있어야한다는 뜻입니다. 표현식의 유효성만을 본다면, 중괄호와 bool을 뺄수도 있습니다. typename type;의 형식이 사용된다면 이는 type이 존재해야만 한다는 의미 입니다. 또한 요구 표현식 내에 요구 절을 추가할 수도 있습니다.

E.3 제약 조건 오버로딩

제약 조건들이 상호 배타적이지 않으며, 하나가 다른 것을 포섭하는 개념도 있습니다. 이는 iterator에서 입력, 순방향과 같이 계층적 구조를 이룰 때 볼 수 있는데, 만약 좀 더 특수화된 쪽, 즉 범위가 더 적은 쪽의 후보를 선호하게 됩니다. 이때 제약 조건이 많은 쪽이 적은 쪽을 포섭한다고 합니다.

E.4 개념 팁

  • 검사 개념으로 정적 단언문에 사용 가능합니다.
  • 이진 호환성을 알아 볼 수 있습니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

부록 C. 오버로딩 해석  (0) 2022.07.08
부록 B. 값 카테고리  (0) 2022.07.08
부록 A. 단정의 법칙(One Definition Rule)  (0) 2022.07.08
28. 템플릿 디버깅  (0) 2022.07.07
27. 표현식 템플릿  (0) 2022.07.07

오버로딩 해석이란 해당 호출 표현식에 대해 호출할 함수를 선택하는 과정으로, 이름을 호출하면 C++ 컴파일러는 추가 정보를 사용해 후보 함수들 중에서 하나를 선택해야 합니다. 대부분 호출 인자의 종류를 활용하여 선택합니다. 이는 C++ 표준화를 거치며 가장 잘 맞는 함수를 선택해야하기 때문에 복잡해졌습니다.

C.1 언제 오버로딩 해석이 적용되는가?

오버 로딩 해석은 함수 호출을 처리하는 과중 일부에 지나지 않으며 모든 함수가 적용되는 것도 아닙니다. 함수 포인터나 멤버 함수에 대한 포인터를 포인터에 의해 결정되므로 해석이 필요 없습니다. 두 번째로 함수 유사 매크로는 오버로딩 될 수 없으며 해석의 범주에 들어가지 않습니다. 함수에 대한 호출은 다음과 같이 이루어집니다.

  1. 이름을 룩업해 초기 오버로딩 집합을 형성
  2. 필요하다면 집합 재조정(템플릿 인자 연역과 치환으로 함수 템플릿 후보가 버려질 수 있습니다)
  3. 후보 함수가 해당 호출에 일치하지 않는다면 오버로딩 집합에서 삭제. 가용 함수 후보 집합 생성
  4. 오버로딩 해석을 통해 가장 잘 맞는 후보 선택. 다수 존재한다면 모호한 호출이 되어 오류
  5. 선택된 후보를 검사하여 오류가 발생한다면 메세지 출력

C.2 간략화된 오버로딩 해석

오버로딩 해석은 호출의 각 인자가 후보의 파라미터와 얼마나 잘 일치되는지를 갖고 가용 후보 함수들의 순위를 매깁니다. 더 나은 후보의 파라미터는 다른 후보의 파라미터보다 덜 일치해서는 안됩니다. 다음을 통해 순위를 매길 수 있습니다.

  1. 완벽한 일치 - 표현식의 형식 혹은 참조자(const, volatile 한정자 정도가 추가)의 형태인 경우
  2. 작은 수정을 통한 일치 - 배열에서 포인터, int**인자를 int const * const *등의 const 추가
  3. 형식 승격을 통한 일치 - int -> long 이나 float -> double 변환
  4. 표준 변환만을 사용해 일치 - 표준 변환이나 파생 클래스를 모호하지 않고 공개된 기본 클래스로 변환하여 호출. 암묵적 호출은 포함하지 않습니다.
  5. 사용자 정의 변환을 통한 일치 - 모든 종류의 암묵적 변환을 허용합니다.
  6. 줄임표를 통한 일치 - 줄임표 파라미터는 거의 모든 형식에 일치할 수 있습니다.

한 가지 예외로는 일반적이지 않은 복사 생성자를 갖는 클래스 형은 가능할 수도 가능하지 않을 수도 있습니다. 오버로딩 해석은 템플릿 인자 연역 이후에 일어나며, 연역 자체는 어떠한 종류의 변환으로도 간주되지 않습니다. 템플릿 인자 연역의 문맥에서, 템플릿 파라미터에 대한 rvalue 참조자는 해당 인자가 lvalue이면 lvalue 참조자형으로도 연역될 수 있고, 인자가 rvalue면 rvalue 참조자형으로 연역될 수도 있습니다.

 정적이 아닌 멤버 함수에 대한 호출은 멤버 함수내에서 *this로 접근할 수 있는 숨겨진 파라미터를 갖습니다. Myclass의 멤버 함수에서 숨겨진 파라미터는 대개 Myclass&나 Myclass const&형을 갖습니다.

 X형식의 인자와 완벽할 일치를 이루는 일반적인 파라미터는 네 종류로, X, X&, X const&, X&&가 존재합니다. 인자에 const가 붙지 않은 버전은 lvalue를, const가 붙은 버전은 rvalue를 선호합니다.

C.3 오버로딩 세부 사항

비템플릿 선호

다른 오버 로딩 해석 측면이 동일하다면 템플릿이 아닌 함수를 더 선호합니다. 두 템플릿 중 하나를 선택해야한다면 더 특수화된 템플릿을 선호합니다.

변환 순서

암묵적 변환은 기초 변환들의 순열로 이뤄집니다. 변환 순열 내에서는 사용자 정의 변환은 최대 한번만 일어날 수 있으며 표준 변환만 포함할수도 있습니다. 다른 변환 순열에 속하는 부분 변환 순열이 더 선호됩니다.

포인터 변환

포인터와 멤버에 대한 포인터는 다음과 같은 다양한 특수 표준 형식 변환을 거칩니다.

  • bool형으로 변환
  • 임의의 포인터형에서 void*로의 변환
  • 포인터일 경우 상속받은 클래스에서 기본 클래스로의 변환
  • 멤버에 대한 포인터인 경우 기본 클래스에서 상속받은 클래스로의 변환

이들은 모두 표준 변환만으로 일치를 만들어 내지만 같은 순위는 아닙니다. bool형으로의 변환은 다른 종류의 표준 변환보다도 더 안 좋은 것으로 취급합니다. 일반 포인터 변환일 경우 상속받은 클래스에서 기본 클래스로의 변환보다 void*형으로의 변환이 더 안 좋은 변환입니다. 이때 상속에 의한 관계가 있는 다른 클래스로의 변환이 가능하다면 가장 하위의 상속 클래스로의 변환을 선호합니다.

초기화자 목록

초기화자 목록 인자는 여러 가지 다양한 파라미터형으로 변환될 수 있습니다. 초기화자 목록의 형식을 암묵적 변환을 수행할 수도 있고, 집합 초기화를 통해 다른 인자를 만들어 낼 수도 있습니다. 초기화자로 초기화할 때의 오버로딩 해석 과정은 다음과 같습니다.

  1. 첫 번째 단계에서는 초기화자 목록 생성자만을 고려합니다. 즉, 기본이 아닌 파라미터 특정 형식 T에 대한 초기화자 생성자만을 고려합니다.
  2. 그런 생성자를 찾을 수 없다면 두번째 단계에서 그 외의 다른 모든 생성자를 고려합니다.

함자와 대리 함수

호출 표현식이 함수 대신 클래스형 객체를 참조한다면 재미있는 상황이 연출됩니다. 오버로딩 집합에는 두가지가 더 추가되늰데, 어떠한 멤버 연산자()라도 집합에 추가되며, 추가되는 클래스형 객체가 포인터를 함수형 암묵적으로 형식 변환하는 연산자가 있는 경우 입니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

부록 E. 개념  (0) 2022.07.08
부록 B. 값 카테고리  (0) 2022.07.08
부록 A. 단정의 법칙(One Definition Rule)  (0) 2022.07.08
28. 템플릿 디버깅  (0) 2022.07.07
27. 표현식 템플릿  (0) 2022.07.07

B.1 전통적인 lvalue와 rvalue

  • lvalue - 메모리나 레지스터에 저장된 실제 값을 가리키는 표현식입니다. 표현식이 할당 시 맡을 수 있는 역할에서 나왔습니다.
  • rvalue - 할당 표현식에서는 오른편에서만 올 수 있습니다.

하지만 이러한 것들은 C, C++이 바뀌면서 정의도 바뀌게 되었습니다.

B.2 C++11 이후의 값 카테고리

이동 의미 체계를 지원하기 위해 rvalue 참조자가 도입되면서 표현식을 lvalue와 rvalue로 나누는 전통적인 구분법만으로는 힘들게 되었습니다. 이에 lvalue, prvalue(pure rvalue), xvalue가 존재합니다. 합성 카테고리로는 glvalue(lvalue, xvalue를 합친 Generalized lvalue) 와 rvalue(xvalue, prvalue)가 존재합니다.

  • glvalue - 객체, 비트 필드나 함수의 정체성을 결정하는 표현식
  • prvalue - 객체나, 비트 필드를 초기화하거나 연산자의 피연산자의 값을 계산하는 표현식
  • xvalue - 객체나 비트 필드로 그 자원이 재활용될 수 있는 glvalue(대개 만료되기 직전)
  • lvalue - xvalue가 아닌 glvalue
  • rvalue - prvalue나 xvalue인 표현식

glvalue는 주소를 각ㅈ는 실체를 생성하여, 이는 자신을 둘러싼 더 큰 객체의 하위 객체에 대한 것일 수도 있습니다. glvalue의 형식은 정적 형식이라 하고, 기본 클래스가 그 일부를 이루는 가장 파생된 클래스의 형식은 glvalue의 동적 형식이라 합니다. glvalue가 기본 클래스 하위 객체를 생성하지 않을 때 정적 형식과 동적 형식이 완전히 동일합니다.

lvalue의 예는 다음과 같습니다.

  • 변수나 함수를 가리키는 표현식
  • 내장 단항 * 연산자의 활용
  • 문자열 리터럴만 있는 표현식
  • lvalue 참조자를 반환하는 함수에 대한 호출

prvalue의 예는 다음과 같습니다

  • 문자열 리터럴이나 사용자 정의 리터럴이 아닌 리터럴로 구성된 표현식
  • 내장 단항 & 연산자의 활용
  • 내장 산술 연산자의 활용
  • 반황형이 참조자형이 아닌 함수에 대한 호출
  • 람다 표현식

xvalue의 예는 다음과 같습니다

  • 객체형에 대한 rvalue 참조자를 반환하는 함수에 대한 호출
  • 객체에 대한 rvalue 참조자로의 형 변환
int x = 3; // x는 변수지만 lvalue가 아닙니다.
           // 3은 변수 x를 초기화 하는 prvalue
int y = x; // x는 lvalue이며 lvalue가 prvalue로 변환되어 y를 초기화.

lvalue는 종종 rvalue로의 변환이 가능한데, prvalue는 객체를 초기화하는 표현식이기 때문입니다. C++17에서 임시 실체화가 도입되면서, glvalue가 있어야 할 자리라면 언제라도 prvalue가 합법적으로 쓰일 수 있고, 임시 객체가 생성돼 그 prvalue로 초기화됩니다. 그리고 그 prvalue는 임시 값을 가리키는 xvalue로 바뀔 수 있습니다.

 임시 값은 다음 상황에서 실체화되고 prvalue로 초기화 됩니다.

  • prvalue는 참조자에 한정됩니다.
  • 클래스 prvalue의 멤버에 접근한 경우
  • 배열 prvalue에 첨자 연산 사용한 경우
  • 배열 prvalue가 첫 번째 요소에 대한 포인터로 변환
  • 어떤 형식 X에 대한 형식 std::initializer_list<X>의 객체를 초기화하는 중괄호 초기화자 목록에 나타난 prvalue
  • prvalue에 적용된 sizeof나 typeid 연산자
  • expr; 형태 명령문 내의 최고 수준 표현식인 prvalue이나 void로 형 변환된 표현식

C++17에서 prvalue로 초기화된 객체는 항상 문맥에 따라서 결정됩니다. 따라서 실체로 필요할 때에만 임시 값이 만들어 집니다.

class N {
  public:
    N();
    N(N const&) = delete;
    N(N&&) = delete;
};

N make_N() {
  return N{};
}

auto n = make_N();

C++17 이전에는 prvalue N{}은 형식이 함수에서 N인 임시 값을 생성하여, 이동 복사가 불가능하여 오류가 발생하지만, C++17부터는 임시 값을 함수 안이 아닌 n의 저장소에 직접 생성하기 때문에 괜찮습니다. 

B.3 decltype으로 값 카테고리 검사

키워드 decltype을 사용하면 어떠한 C++ 표현식에 대해서든 값 카테고리 검사가 가능합니다.

  • x가 prvalue면 type
  • x가 lvalue면 type&
  • x가 xvalue면 type&&

표현식 x가 실체를 가리키는 이름일 경우 이름이 붙여진 바로 그 실체의 선언된 형식이 아닌 표현식 x의 형식을 얻고 싶다면 decltype((x))를 사용해야 합니다.

B.4 참조자형

C++에서의 참조자형은 두가지 중요한 방식으로 값 카테고리와 관련되어 있습니다.

 먼저 참조자는 표현식이 가질 수 있는 값 카테고리를 한정 짓습니다. 예를 들어 int& 형식에 대한 const가 아닌 lvalue 참조자는 int형의 lvalue인 표현식으로만 초기화 될 수 있습니다. 비슷하게 int&&형의 rvalue 참조자는 int형의 rvalue인 표현식으로만 초기화될 수 있습니다.

 두 번째로는 함수의 반황형으로, 참조자형을 반환형으로 사용하면 극 함수에 대한 호출의 값 카테고리에 영향을 미칩니다.

  • 반황형이 lvalue 참조자인 함수를 호출하면 lvalue가 만들어 집니다.
  • 반환형이 객체형에 대한 rvalue 참조자인 함수를 호출하면 xvalue가 만들어집니다.
  • 반환형이 참조자가 아닌 형식을 반환하는 함수를 호출하면 prvalue가 만들어집니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

부록 E. 개념  (0) 2022.07.08
부록 C. 오버로딩 해석  (0) 2022.07.08
부록 A. 단정의 법칙(One Definition Rule)  (0) 2022.07.08
28. 템플릿 디버깅  (0) 2022.07.07
27. 표현식 템플릿  (0) 2022.07.07

ODR로 알려진 단정의 법칙은 잘 구성된 C++ 프로그램 구조의 주춧돌로, 인라인이 아닌 함수는 단 한번만 정의되고 클래스와 인라인 함수는 번역 단위 내에서 최대 한번만 정의될 수 있어, 같은 실체에 대한 모든 정의를 같게 합니다. 하지만 템플릿 인스턴스와 함께 세부적으로 들어가면 상당히 복잡해집니다.

A.1 번역 단위

C++ 프로그램을 작성할 때 파일들은 코드로 구성되는데, 파일의 경계보다는 번역 단위가 ODR에 있어 중요합니다. 번역 단위는 전처리자가 제 할일을 마친 후 컴파일러에게 넘긴 코드 덩어리를 일컫습니다. 전처리자는 컴파일 지시자와 주석을 수행하고, #include로 추가된 파일을 삽입하고 매크로를 확장합니다. 만약 두 번역 단위 사이에 외부 링크를 갖는 대응 선언을 갖는다면, 번역 단위 경계를 넘는 연결이 생깁니다.

A.2 선언과 정의

ODR에서 선언과 정의의 정확한 의미는 매우 중요합니다.

선언은 프로그램에 이름을 도입 혹은 재도입하는 C++ 생성 과정입니다. 선언은 어떤 실체를 어떻게 도입할 것인가에 따라 정의됩니다.

  • 네임스페이스와 네임스페이스 별칭 - 네임 스페이스와 그들의 별칭에 대한 선언은 항상 정의입니다. 네임 스페이스의 멤버들의 목록은 이후에 확장될 수 있기 떄문입니다.
  • 클래스, 클래스 템플릿, 함수, 함수 템플릿, 멤버 함수와 멤버 함수 템플릿 - 선언이 중괄호{}로 묶인 내용을 가진 이름으로 이뤄졌을 경우에만 정의입니다. (선언만 한 경우 X) 이는 공용체, 연산자 등의 템플릿 버전에 대한 명시적 특수화에도 적용됩니다.
  • 열거형 - 열거될 값들을 나열한 목록이 중괄호로 묶여 뒤따라올 때에만 선언은 정의와 동일합니다.
  • 지역 변수와 정적이지 않은 데이터 멤버 - 이들은 항상 정의로 취급되며, 그차이는 거의 문제가 되지 않습니다. 함수 정의 안에서 함수 파라미터를 선언하는 것 자체는 정의며 지역 변수를 나타내지만, 정의가 아닌 함수 선언 내에 있는 함수 파라미터는 정의가 아닙니다.
  • 전역 변수 - 선언이 extern 바로 뒤에 나오지 않았거나 초기화자를 갖고 있다면, 전역 변수의 선언은 해당 변수에 대한 정의이지만, 그렇지 않다면 정의가 아닙니다.
  • 정적 데이터 멤버 - 자신이 멤버로 속해있는 클래스 혹은 클래스 템플릿의 외부에서 선언된 경우나 클래스나 클래스 템플릿에서 inline이나 constexpr로 선언된 경우만 정의입니다.
  • 명시적이고 부분적인 특수화 - template<>이나 template<...>이 붙은 선언이 정의일 경우 해당 선언은 정의입니다. 정적 데이터 멤버의 명시적 특수화나 정적 데이터 멤버 템플릿은 초기화자가 포함될 경우에만 정의입니다.

그 외의 선언은 정의가 아니며, 형식 별칭, using 선언, using 지시자, 템플릿 파라미터 선언, 명시적 인스턴스화 지시자, static_assert 선언 등은 정의가 아닙니다.

A.3 상세한 단정의 법칙

A.3.1 프로그램당 한번

다음의 아이템들은 프로그램 당 단 한번만 정의돼야 합니다.

  • 인라인이 아닌 함수와 인라인이 아닌 멤버 함수
  • 인라인이 아닌 변수
  • 인라인이 아닌 정적 데이터 멤버

이는 다른 네임스페이스와 같이 내부 링크를 갖는 실체(이름 없는 네임스페이스 혹은 static 명시자가 존재하는 실체들)라면 적용되지 않습니다. 그러한 실체가 동일한 이름을 갖는다 하더라도 이들은 서로 다른 것으로 간주됩니다. 같은 맥락으로 이름 없는 네임스페이스에서 선언됐더라도 서로 다른 번역 단위에 나타난 실체라면 서로 다른 것으로 간주됩니다.

http://egloos.zum.com/sweeper/v/3082953 참조

만약 이번 절에서 살펴본 제약 조건을 어기더라도, C++ 컴파일러/링커가 진단 메세지를 출력하지 않고 정의 중첩 혹은 정의 없음을 보고합니다.

A.3.2 번역 단위별 한번 정의 제약

어떤 실체도 번역 단위 내에서 한 번 이상 정의될 수 없습니다. 이를 위해서 헤더 파일의 코드는 가드로 둘러쌉니다. ODR은 특정 실체가 특정 환경에서 정의돼야만 한다고 명시합니다. 이 법칙은 클래스형, 인라인 함수와 내보내기되지 않은 템플릿에서도 마찬가지로 정의됩니다.

 클래스형 X는 사용되기 전에 해당 번역 단위에서 다음과 같은 방식으로 정의되어야만 합니다.

  • X형의 객체 생성. 이때 X형의 객체를 포함하는 객체가 생성되는 것과 같은 간접적인 생성도 포함됩니다.
  • X형의 데이터 멤버 선언
  • X형 객체에 대한 sizeof나 typeid 사용
  • X형의 멤버에 명시적으로나 암묵적으로 접근
  • 표현식을 통해 X형으로 바꾸거나 X형에서 바뀌는 경우, 이에 해당하는 포인터나 참조자를 바꾸는 경우
  • X형의 객체에 값 할당
  • X형의 인자를 갖는 함수 혹은 X형을 반환하는 함수를 정의하거나 호출할 경우. 선언만 하는 경우는 해당X

형식에 대한 법칙은 클래스 템플릿에서 생성된 X형에도 동일하게 적용되므로, X형이 선언돼야만 하는 상황에서 대응되는 템플릿은 꼭 정의돼야만 합니다. 인라인 함수는 모든 번역 단위에서 정의돼야만 하지만, 이는 클래스형과는 달리 이들의 정의는 사용 지점 이후에 나타나도 괜찮습니다.

A.3.3 교차 번역 단위 동등 제약

한 번역 단위보다 더 많은 곳에서 특정 실체를 정의할 수 있게 되면 일치하지 않은 다중 정의가 생겨 오류가 발생할 수 있습니다. 교차 번역 단위 제약 조건에 따르면 실체가 두 군데에서 정의됐을 때 두 장소는 정확히 같은 토큰의 순열을 가져야 합니다. 더구나 이러한 토큰들은 서로의 문맥에서 동일한 의미를 가져야만 합니다.

static int counter = 0;
inline void increaseCounter() { ++counter; };

////
static int counter = 2;
inline void increaseCounter() { ++counter; };

다음 두 함수는 다른 counter 토큰을 사용하기에 오류가 발생합니다. 이러한 실체에 대한 정의를 헤더 파일을 두어 필요할 때마다 #include로 포함시키려 할 때는 거의 모든 상황에서 토큰 순서가 같도록 주의해야 합니다.

// 번역 단위 1
class X {
  public:
    X(int, int);
    X(int, int, int);
};

X::X(int, int = 0) {};

class D {
  X x = 0;
};

D d1; // X(int, int) called by D()


// 번역 단위 2
class X {
  public:
    X(int, int);
    X(int, int, int);
};

X::X(int, int = 0, int = 0) {};

class D {
  X x = 0;
};

D d2; // X(int, int, int) called by D()

동일한 토큰은 동일한 실체를 가리켜야 한다는 법칙에도 예외가 존재하는데, 동일한 토큰이 같은 값을 갖고 결과 표현식의 주소를 사용하지 않는 상수를 가르킨다면 두 토큰은 동일한 것으로 간주합니다.

 템플릿의 경우, 템플릿에서 이름은 두 단계 바인드됩니다. 종속되지 않은 이름은 템플릿이 정의되는 시점에 바인드되며, 이런 이름들의 경우 동등 법칙은 다른 비템플릿의 정의들과 유사하게 처리되지만, 인스턴스화 시점에 바인드되는 이름들은 그 지점에서 동등 법칙이 적용돼야만 하고, 바인딩은 동등해야만 합니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

부록 C. 오버로딩 해석  (0) 2022.07.08
부록 B. 값 카테고리  (0) 2022.07.08
28. 템플릿 디버깅  (0) 2022.07.07
27. 표현식 템플릿  (0) 2022.07.07
26. 구별 공용체  (0) 2022.07.07

템플릿을 디버깅할 때 부닥치게 되는 문제는 크게 2가지로 하나는 템플릿 작성자의 문제로 문서화한 조건을 만족시키는 템플릿 인자를 위해서도 동작할 수 있다는 것에 대한 확신의 문제이고, 다른 하나는 사용자가 템플릿을 어긴 채 사용했을 때, 어떤 항목을 어겼는지 찾아낼 수 있는 지의 문제입니다.

 이는 개념이라는 용어를 통해 C++20부터는 좀 더 더 쉽게 디버깅할 수 있습니다만 이번 장에서는 자세하게 다루지는 않습니다.

28.1 얕은 인스턴스화

계층적으로 구성된 클래스에서 하단에서 오류가 발생한다면 이는 인스턴스화 때에만 검출될 수가 있습니다. 이를 확인하기 위해서 조기에 문법을 확인하거나 파라미터 사용을 활용하는 방식이 존재합니다.

28.2 정적 단언문

assert() 매크로는 C++ 코드에서 프로그램의 실행 내 특정 지점에서 어떤 조건이 만족되는지 검사하는 용도로 자주 사용되는데, 단언문이 실패하면 프로그램은 중단돼 프로그래머가 그 문제를 고칠 수 있게 됩니다. C++11에 추가된 static_assert 키워드는 컴파일 과정에 평가되어 컴파일러가 오류 메세지를 발생시킵니다. 이를 통해 어떤 형식이 역참조 가능한지를 알아보는 형식 특질을 만들 수 있습니다.

#include <utility>        // for declval()
#include <type_traits>    // for true_type and false_type

template<typename T>
class HasDereference {
 private:
  template<typename U> struct Identity;
  template<typename U> static std::true_type 
    test(Identity<decltype(*std::declval<U>())>*);
  template<typename U> static std::false_type
    test(...);
 public:
  static constexpr bool value = decltype(test<T>(nullptr))::value;
};

정적 단언문을 쓰면 오류 메세지가 짧고 직접적인 장점을 지닙니다. 클래스 템플릿에서도 정적 단언문을 쓸 수 있으며 형식 특질을 활용할 수도 있습니다.

28.3 원형

템플릿을 만들 때 그 템플릿에 대해 명시된 제약 조건을 만족시키는 템플릿 인자 모두에 대해 템플릿 정의가 되게 하는 건 상당히 까다로운 일인데, 이를 사전에 확인하거나 그에 맞춰 원형을 개발해야할 것입니다.

28.4 추적자

이제 까지는 템플릿을 포함한 프로그램을 컴파일하거나 링크할 때 생기는 버그에 대해 알아보았지만, 빌드가 성공하더라도 올바르게 돌아가는지에 대한 확인 작업도 필요합니다. 추적자는 개발 초기에 템플릿 정의에서 문제를 검출함으로써 디버깅 문제를 완화시킬 수 있는 소프트웨어 도구로, 추적자는 템플릿의 요구 사항만을 만족시키게 작성됩니다. 아래는 정렬 알고리즘을 검사하기 위한 추적자 예제입니다.

class SortTracer {
  private:
    int value;                           // integer value to be sorted
    int generation;                      // generation of this tracer
    inline static long n_created = 0;    // number of constructor calls
    inline static long n_destroyed = 0;  // number of destructor calls
    inline static long n_assigned = 0;   // number of assignments
    inline static long n_compared = 0;   // number of comparisons
    inline static long n_max_live = 0;   // maximum of existing objects

    // recompute maximum of existing objects
    static void update_max_live() {
        if (n_created-n_destroyed > n_max_live) {
            n_max_live = n_created-n_destroyed;
        }
    }

  public:
    static long creations() {
        return n_created;
    }
    static long destructions() {
        return n_destroyed;
    }
    static long assignments() {
        return n_assigned;
    }
    static long comparisons() {
        return n_compared;
    }
    static long max_live() {
        return n_max_live;
    }

  public:
    // constructor
    SortTracer (int v = 0) : value(v), generation(1) {
        ++n_created;
        update_max_live();
        std::cerr << "SortTracer #" << n_created
                  << ", created generation " << generation
                  << " (total: " << n_created - n_destroyed
                  << ")\n";
    }

    // copy constructor
    SortTracer (SortTracer const& b)
     : value(b.value), generation(b.generation+1) {
        ++n_created;
        update_max_live();
        std::cerr << "SortTracer #" << n_created
                  << ", copied as generation " << generation
                  << " (total: " << n_created - n_destroyed
                  << ")\n";
    }

    // destructor
    ~SortTracer() {
        ++n_destroyed;
        update_max_live();
        std::cerr << "SortTracer generation " << generation
                  << " destroyed (total: "
                  << n_created - n_destroyed << ")\n";
    }

    // assignment
    SortTracer& operator= (SortTracer const& b) {
        ++n_assigned;
        std::cerr << "SortTracer assignment #" << n_assigned
                  << " (generation " << generation
                  << " = " << b.generation
                  << ")\n";
        value = b.value;
        return *this;
    }

    // comparison
    friend bool operator < (SortTracer const& a,
                            SortTracer const& b) {
        ++n_compared;
        std::cerr << "SortTracer comparison #" << n_compared
                  << " (generation " << a.generation
                  << " < " << b.generation
                  << ")\n";
        return a.value < b.value;
    }

    int val() const {
        return value;
    }
};

여기서 generation == 1인 경우는 원본, generation == 2는 원본에서 복사한 복사본이며 계속 늘어나게 됩니다. 이를 통해 다음과 같이 테스트 가능합니다. 

#include <iostream>
#include <algorithm>
#include "tracer.hpp"

int main()
{
    // prepare sample input:
    SortTracer input[] = { 7, 3, 5, 6, 4, 2, 0, 1, 9, 8 };

    // print initial values:
    for (int i=0; i<10; ++i) {
        std::cerr << input[i].val() << ' ';
    }
    std::cerr << '\n';

    // remember initial conditions:
    long created_at_start = SortTracer::creations();
    long max_live_at_start = SortTracer::max_live();
    long assigned_at_start = SortTracer::assignments();
    long compared_at_start = SortTracer::comparisons();

    // execute algorithm:
    std::cerr << "---[ Start std::sort() ]--------------------\n";
    std::sort<>(&input[0], &input[9]+1);
    std::cerr << "---[ End std::sort() ]----------------------\n";

    // verify result:
    for (int i=0; i<10; ++i) {
        std::cerr << input[i].val() << ' ';
    }
    std::cerr << "\n\n";

    // final report:
    std::cerr << "std::sort() of 10 SortTracer's"
              << " was performed by:\n "
              << SortTracer::creations() - created_at_start
              << " temporary tracers\n "
              << "up to "
              << SortTracer::max_live()
              << " tracers at the same time ("
              << max_live_at_start << " before)\n "
              << SortTracer::assignments() - assigned_at_start
              << " assignments\n "
              << SortTracer::comparisons() - compared_at_start
              << " comparisons\n\n";
}

'C++공부 > C++ Templates' 카테고리의 다른 글

부록 B. 값 카테고리  (0) 2022.07.08
부록 A. 단정의 법칙(One Definition Rule)  (0) 2022.07.08
27. 표현식 템플릿  (0) 2022.07.07
26. 구별 공용체  (0) 2022.07.07
25. 튜플  (0) 2022.07.07

표현식 템플릿이라 불리는 템플릿 프로그래밍 기술로 숫자 배열 클래스를 지원하기 위해 고안되었습니다. 크기가 큰 배열을 계산할 때, 표현식 템플릿을 통해 빠르게 원하는 것을 얻을 수 있으며, 이는 템플릿 메타프로그래밍과 같이 재귀적으로 풀어나갑니다.

27.1 임시 루프와 루프 분할

먼저 기본 배열 템플릿 SArray와 그에 따른 산술 연산자는 다음과 같습니다.

template<typename T>
class SArray {
  public:
    // create array with initial size
    explicit SArray (std::size_t s)
     : storage(new T[s]), storage_size(s) {
        init();
    }

    // copy constructor
    SArray (SArray<T> const& orig)
     : storage(new T[orig.size()]), storage_size(orig.size()) {
        copy(orig);
    }

    // destructor: free memory
    ~SArray() {
        delete[] storage;
    }

    // assignment operator
    SArray<T>& operator= (SArray<T> const& orig) {
        if (&orig!=this) {
            copy(orig);
        }
        return *this;
    }

    // return size
    std::size_t size() const {
        return storage_size;
    }

    // index operator for constants and variables
    T const& operator[] (std::size_t idx) const {
        return storage[idx];
    }
    T& operator[] (std::size_t idx) {
        return storage[idx];
    }

  protected:
    // init values with default constructor
    void init() {
        for (std::size_t idx = 0; idx<size(); ++idx) {
            storage[idx] = T();
        }
    }
    // copy values of another array
    void copy (SArray<T> const& orig) {
        assert(size()==orig.size());
        for (std::size_t idx = 0; idx<size(); ++idx) {
            storage[idx] = orig.storage[idx];
        }
    }

  private:
    T*          storage;       // storage of the elements
    std::size_t storage_size;  // number of elements
};


// additive assignment of SArray
template<typename T>
SArray<T>& SArray<T>::operator+= (SArray<T> const& b)
{
    assert(size()==orig.size());
    for (std::size_t k = 0; k<size(); ++k) {
        (*this)[k] += b[k];
    }
    return *this;
}

// multiplicative assignment of SArray
template<typename T>
SArray<T>& SArray<T>::operator*= (SArray<T> const& b)
{
    assert(size()==orig.size());
    for (std::size_t k = 0; k<size(); ++k) {
        (*this)[k] *= b[k];
    }
    return *this;
}

// multiplicative assignment of scalar
template<typename T>
SArray<T>& SArray<T>::operator*= (T const& s)
{
    for (std::size_t k = 0; k<size(); ++k) {
        (*this)[k] *= s;
    }
    return *this;
}

만약 단순히 더하기 곱하기 연산자를 사용하여 임시 배열을 만들어서 반환을 했다면, 상당히 비효율적으로 됩니다. 특별히 빠른 할당자가 사용되지 않는 한 작은 배열의 연산에 소모되는 시간의 대부분은 필요 없는 임시 배열을 생성하는데 사용됩니다. 따라서 계산 할당자를 사용하면 좀 더 효율적입니다. 하지만 표기 방식이 이상하고, 기존의 값을 위해서는 임시 배열이 필요합니다. 즉 위와 같은 표기는 상당히 비효율 적입니다.

27.2 템플릿 인자에 표현식 표현

전체 표현식을 다 읽을 때까지 표현식의 일부만 계산하지 않는다면 앞서 살펴본 문제를 해결할 수 있습니다. 이를 위해서는 계산하기 전 어떤 객체에 무슨 연산이 적용됐는지 기록해두고, 적용될 연산은 컴파일 과정에 결정될 수 있기에, 템플릿 인자로도 표현할 수 있습니다. 표현식에 대한 설명을 완성하려면 객체에 대한 인자의 참조자를 저장하고, 저장될 값을 기록해야 합니다. 이는 다음과 같이 표현 가능합니다.

template<typename T> class A_Scalar;

// primary template
template<typename T>
class A_Traits {
  public:
    using ExprRef = T const&;     // type to refer to is constant reference
};

// partial specialization for scalars
template<typename T>
class A_Traits<A_Scalar<T>> {
  public:
    using ExprRef = A_Scalar<T>;  // type to refer to is ordinary value
};

// class for objects that represent the addition of two operands
template<typename T, typename OP1, typename OP2>
class A_Add {
  private:
    typename A_Traits<OP1>::ExprRef op1;    // first operand
    typename A_Traits<OP2>::ExprRef op2;    // second operand

  public: 
    // constructor initializes references to operands
    A_Add (OP1 const& a, OP2 const& b)
     : op1(a), op2(b) {
    }

    // compute sum when value requested
    T operator[] (std::size_t idx) const {
        return op1[idx] + op2[idx];
    }

    // size is maximum size
    std::size_t size() const {
        assert (op1.size()==0 || op2.size()==0
                || op1.size()==op2.size());
        return op1.size()!=0 ? op1.size() : op2.size();
    }
};

// class for objects that represent the multiplication of two operands
template<typename T, typename OP1, typename OP2>
class A_Mult {
  private:
    typename A_Traits<OP1>::ExprRef op1;    // first operand
    typename A_Traits<OP2>::ExprRef op2;    // second operand

  public:
    // constructor initializes references to operands
    A_Mult (OP1 const& a, OP2 const& b)
     : op1(a), op2(b) {
    }

    // compute product when value requested
    T operator[] (std::size_t idx) const {
        return op1[idx] * op2[idx];
    }

    // size is maximum size
    std::size_t size() const {
        assert (op1.size()==0 || op2.size()==0
                || op1.size()==op2.size());
        return op1.size()!=0 ? op1.size() : op2.size();
    }
};

여기서 AScalar 템플릿은 다음과 같이 정의됩니다.

// class for objects that represent scalars:
template<typename T>
class A_Scalar {
  private:
    T const& s;  // value of the scalar

  public:
    // constructor initializes value
    constexpr A_Scalar (T const& v)
     : s(v) {
    }

    // for index operations, the scalar is the value of each element
    constexpr T const& operator[] (std::size_t) const {
        return s;
    }

    // scalars have zero as size
    constexpr std::size_t size() const {
        return 0;
    };
};

 

...리를 하다보니 책을 보는게 더 낫다는 판단을 하였습니다. 책에서 말하는 내용을 더 크게 압축할 것도 없고 표현식을 장황하게 설명해야하기에 정리는 이정도로만 하고 책을 더 보는 걸로 마무리 짓겠습니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

부록 A. 단정의 법칙(One Definition Rule)  (0) 2022.07.08
28. 템플릿 디버깅  (0) 2022.07.07
26. 구별 공용체  (0) 2022.07.07
25. 튜플  (0) 2022.07.07
24. 형식 목록  (0) 2022.07.06

튜플은 여러 가지 형식의 값을 하나의 값으로 모아 저장할 수 있게 해 간단한 구조체처럼 동작합니다. 따라서, 단 하나의 값을 갖지만 그 값의 형식은 가질 수 있는 형식 목록 중 하나를 가지도록 Variant를 구현할 것인데 이는 std::variant<>와 같은 역할을 할 것입니다.

26.1 저장소

Variant 형식을 설계할 때 가장 중요하게 여겨야 할 점은 바로 활성화된 값으로, 저장된 값의 저장소를 어떻게 관리할 지를 고려해야 합니다. 뿐만 아니라 변이 값은 현재 활성화된 값의 형식이 뭔지 알리는 구별자도 저장해야 합니다.

 만약 이를 튜플로 구현한다면 상당히 비효율 적입니다. 값 하나를 저장해야는데 저장소의 크기는 값 형식의 크기를 모두 합친 것만큼 커야합니다. 따라서 이번에는 클래스가 아닌 공용체를 사용합니다.

template<typename... Types>
class VariantStorage {
  using LargestT = LargestType<Typelist<Types...>>;
  alignas(Types...) unsigned char buffer[sizeof(LargestT)];
  unsigned char discriminator = 0;
 public:
  unsigned char getDiscriminator() const { return discriminator; }
  void setDiscriminator(unsigned char d) { discriminator = d; }

  void* getRawBuffer() { return buffer; }
  const void* getRawBuffer() const { return buffer; }

  template<typename T>
    T* getBufferAs() { return std::launder(reinterpret_cast<T*>(buffer)); }
  template<typename T>
    T const* getBufferAs() const {
      return std::launder(reinterpret_cast<T const*>(buffer));
    }
};

template<typename Head, typename... Tail>
union VariantStorage<Head, Tail...> {
  Head head;
  VariantStorage<Tail...> tail;
};

template<>
union VariantStorage<> {};

공용체를 사용하면 Types 내 어떤 형식 값도 저장가능하며, 공간도 충분하고 정렬도 보장됩니다. 하지만 공용체가 다루기 어려운데 이는 상속이 되지 않기 때문입니다. 먼저 가장 큰 공간을 구하기 위해 LargestType으로 가장 큰 타입을 통해 공간을 확보하고, getBuffer를 통해 버퍼에 대한 포인터를 얻고 명시적 형식 변환을 통해 저장소를 조작, 저장을 합니다. 이는 std::launder를 통해 인자를 수정하지 않고 돌려줄 수 있게 합니다.

26.2 설계

Variant 형식은 상속이 제공되지 않기 때문에, CRTP를 사용하여 가장 많이 파생된 형식을 사용해 공유 변이 값 저장소에 접근합니다. 또한 파라미터 꾸러미에서 Types를 통해 특정 형식 T가 있는 곳을 알아내는 FindIndexOfT 메타함수도 구현하였습니다.

template<typename List, typename T, unsigned N = 0, 
         bool Empty = IsEmpty<List>::value>
struct FindIndexOfT;

// recursive case:
template<typename List, typename T, unsigned N>
struct FindIndexOfT<List, T, N, false> 
 : public IfThenElse<std::is_same<Front<List>, T>::value,
                     std::integral_constant<unsigned, N>,
                     FindIndexOfT<PopFront<List>, T, N+1>>
{};

// basis case:
template<typename List, typename T, unsigned N>
struct FindIndexOfT<List, T, N, true>
{};

template<typename T, typename... Types>
class VariantChoice {
  using Derived = Variant<Types...>;
  Derived& getDerived() { return *static_cast<Derived*>(this); }
  Derived const& getDerived() const {
    return *static_cast<Derived const*>(this);
  }
 protected:
  // compute the discriminator to be used for this type
  constexpr static unsigned Discriminator =
    FindIndexOfT<Typelist<Types...>, T>::value + 1;
 public:
  VariantChoice() { }
  VariantChoice(T const& value);          // see variantchoiceinit.hpp
  VariantChoice(T&& value);               // see variantchoiceinit.hpp
  bool destroy();                         // see variantchoicedestroy.hpp
  Derived& operator= (T const& value);    // see variantchoiceassign.hpp
  Derived& operator= (T&& value);         // see variantchoiceassign.hpp
};

...정리를 하다보니 책을 보는게 더 낫다는 판단을 하였습니다. 책에서 말하는 내용을 더 크게 압축할 것도 없고 표현식을 장황하게 설명해야하기에 정리는 이정도로만 하고 책을 더 보는 걸로 마무리 짓겠습니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

28. 템플릿 디버깅  (0) 2022.07.07
27. 표현식 템플릿  (0) 2022.07.07
25. 튜플  (0) 2022.07.07
24. 형식 목록  (0) 2022.07.06
23. 메타프로그래밍  (0) 2022.07.06

템플릿의 강력함을 보이기 위해서는 동종 컨테이너 뿐 아니라 배열과 유사한 형식들을 많이 사용하였는데, C++에서 동종이 아닌 저장 공간으로 클래스과 구조체가 존재합니다. 이와 유사하게 데이터를 모으는 방식으로 튜플이 존재하는데, 이는 클래스와 유사하지만 튜플은 요소를 위치로 참조하는데 반해 구조체는 이름으로 참조합니다. 위치를 이용한 인터페이스가 형식 목록에서 튜플을 보다 쉽게 만들기에 템플릿 메타 프로그래밍 기법에서 구조체 보다 튜플이 적합합니다. 이번 장에서는 std::tuple을 간략화한 Tuple 클래스를 만들어 이를 알아볼 예정입니다.

25.1 기본 튜플 설계

튜플은 함수 템플릿 get을 통해 접근하여 참조자를 반환 받습니다.

template<typename... Types>
class Tuple;

// recursive case:
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
{
 private:
  Head head;
  Tuple<Tail...> tail;
 public:
  // constructors:
  Tuple() {
  }
  Tuple(Head const& head, Tuple<Tail...> const& tail)
    : head(head), tail(tail) {
  }
  //...

  Head& getHead() { return head; }
  Head const& getHead() const { return head; }
  Tuple<Tail...>& getTail() { return tail; }
  Tuple<Tail...> const& getTail() const { return tail; }
};

// basis case:
template<>
class Tuple<> {
  // no storage required
};

재귀 상황에서 각 Tuple 인스턴스에는 목록 내의 첫 번재 요소를 저장하는 데이터 멤버인 head와 목록 내의 나머지 요소를 저장하는 데이터 멤버인 tail이 존재 합니다. 기본 상황은 비어있는 튜플이므로 따로 저장소가 존재하지 않습니다. get 함수는 호출을 위해 재귀적으로 수행됩니다.

// recursive case:
template<unsigned N>
struct TupleGet {
  template<typename Head, typename... Tail>
  static auto apply(Tuple<Head, Tail...> const& t) {
    return TupleGet<N-1>::apply(t.getTail());
  }
};

// basis case:
template<>
struct TupleGet<0> {
  template<typename Head, typename... Tail>
  static Head const& apply(Tuple<Head, Tail...> const& t) {
    return t.getHead();
  }
};

template<unsigned N, typename... Types>
auto get(Tuple<Types...> const& t) {
  return TupleGet<N>::apply(t);
}

튜플의 생성자는 유용하지만, 독립적인 값 집합과 다른 튜플에서 튜플을 생성하기를 원합니다. 특히, 사용자는 요소의 일부 값 초기화할 때 이동 생성하기를 원할 수도 있으며, 다양한 형식의 값에서 생성된 요소를 원할 수도 있습니다. 따라서 완벽한 전달을 사용할 필요가 있습니다. 또한 튜플을 통해 다른 튜플을 생성하기를 원하지만 이때 템플릿을 enable_if로 적절하게 제어할 필요가 있습니다.

25.2 기본 튜플 연산

튜플의 비교는 다음과 같이 진행될 수 있습니다.

// basis case:
bool operator==(Tuple<> const&, Tuple<> const&)
{
  // empty tuples are always equivalent
  return true;
}

// recursive case:
template<typename Head1, typename... Tail1,
         typename Head2, typename... Tail2,
         typename = std::enable_if_t<sizeof...(Tail1)==sizeof...(Tail2)>>
bool operator==(Tuple<Head1, Tail1...> const& lhs,
                Tuple<Head2, Tail2...> const& rhs)
{
  return lhs.getHead() == rhs.getHead() &&
         lhs.getTail() == rhs.getTail();
}

이도 재귀적으로 이루어지며, 출력도 재귀적으로 수행할 수 있습니다.

25.3 튜플 알고리즘

튜플은 각 요소에 접근하고 수정할 수 있는 컨테이너일 뿐만 아니라, 새로운 튜플을 생성하고 분리할 수도 있습니다. 따라서 이를 통해 추가,제거, 재배열 및 하위 집합 선택 등의 여러 알고리즘을 만들 수 있습니다. 이는 앞서 TypeList에서 한 것과 동일하게 수행될 수 있습니다.

// basis case
template<typename V>
Tuple<V> pushBack(Tuple<> const&, V const& value)
{
  return Tuple<V>(value);
}

// recursive case
template<typename Head, typename... Tail, typename V>
Tuple<Head, Tail..., V>
pushBack(Tuple<Head, Tail...> const& tuple, V const& value) 
{
  return Tuple<Head, Tail..., V>(tuple.getHead(),
                                 pushBack(tuple.getTail(), value));
}

또한 역전 알고리즘도 이와 같이 재귀를 통해 구성할 수 있습니다. 하지만 이와 같이 비슷하게 역전 알고리즘을 짠다면, 자가 복제가 상당히 많이 일어나기 때문에, 비효율 적입니다. 따라서 한 튜플에 대해서만 역전 연산을 구현하면 효율적으로 구현이 가능한데, 이는 인덱스 목록을 표현하는 std::integer_sequence를 통해 수행 가능합니다.

 먼저 다음과 같이 역순의 인덱스 목록을 얻는 템플릿을 만들고 이를 통해, 역으로 하나씩 뒤에서부터 앞으로 구축해나가는 식으로 해결해나갈 수 있습니다.

// recursive case
template<unsigned N, typename Result = Valuelist<unsigned>>
struct MakeIndexListT
 : MakeIndexListT<N-1, PushFront<Result, CTValue<unsigned, N-1>>>
{
};

// basis case
template<typename Result>
struct MakeIndexListT<0, Result>
{
  using Type = Result;
};

template<unsigned N>
using MakeIndexList = typename MakeIndexListT<N>::Type;

template<typename... Elements, unsigned... Indices>
auto reverseImpl(Tuple<Elements...> const& t, 
                 Valuelist<unsigned, Indices...>)
{
  return makeTuple(get<Indices>(t)...);
}

template<typename... Elements>
auto reverse(Tuple<Elements...> const& t) 
{
  return reverseImpl(t, 
                     Reverse<MakeIndexList<sizeof...(Elements)>>());
}

이와 비슷하게 특정 위치의 튜플도 가져올 수 있습니다. 이를 확장하여 splat이라는 함수는 가져온 형식을 정해진 수 만큼 복사하는데 이는 다음과 같이 구현 가능합니다.

template<unsigned I, unsigned N, typename IndexList = Valuelist<unsigned>>
class ReplicatedIndexListT;

template<unsigned I, unsigned N, unsigned... Indices>
class ReplicatedIndexListT<I, N, Valuelist<unsigned, Indices...>>
 : public ReplicatedIndexListT<I, N-1,
                               Valuelist<unsigned, Indices..., I>> {
};

template<unsigned I, unsigned... Indices>
class ReplicatedIndexListT<I, 0, Valuelist<unsigned, Indices...>> {
 public:
  using Type = Valuelist<unsigned, Indices...>;
};

template<unsigned I, unsigned N>
using ReplicatedIndexList = typename ReplicatedIndexListT<I, N>::Type;

template<unsigned I, unsigned N, typename... Elements>
auto splat(Tuple<Elements...> const& t) 
{
  return select(t, ReplicatedIndexList<I, N>());
}

이와 같이 수행하면 정렬도 가능합니다.

// metafunction wrapper that compares the elements in a tuple:
template<typename List, template<typename T, typename U> class F>
class MetafunOfNthElementT {
 public:
  template<typename T, typename U> class Apply;

  template<unsigned N, unsigned M> 
  class Apply<CTValue<unsigned, M>, CTValue<unsigned, N>>
    : public F<NthElement<List, M>, NthElement<List, N>> { };
};

// sort a tuple based on comparing the element types:
template<template<typename T, typename U> class Compare,
         typename... Elements>
auto sort(Tuple<Elements...> const& t)
{
  return select(t, 
                InsertionSort<MakeIndexList<sizeof...(Elements)>,
                              MetafunOfNthElementT<
                                         Tuple<Elements...>,
                                         Compare>::template Apply>());
}

25.4 튜플 확장

튜플은 서로 관련 있는 값들을 한 값으로 묶을 때 유용한데, 어느 순간에 이르면 이를 풀어야 합니다. 튜플을 풀어 낼려면 인덱스 목록을 사용하여 이를 재귀적으로 풀어 나가야만 합니다.

template<typename F, typename... Elements, unsigned... Indices>
auto applyImpl(F f, Tuple<Elements...> const& t,
                    Valuelist<unsigned, Indices...>)
  ->decltype(f(get<Indices>(t)...))
{
  return f(get<Indices>(t)...);
}

template<typename F, typename... Elements, 
         unsigned N = sizeof...(Elements)>
auto apply(F f, Tuple<Elements...> const& t) 
  ->decltype(applyImpl(f, t, MakeIndexList<N>()))
{
  return applyImpl(f, t, MakeIndexList<N>());
}

25.5 튜플 최적화

튜플은 많은 공간을 차지하는데 그 중에서도 Tail이 빈 튜플로 끝나 공간을 차지합니다. 이를 해결하려면 꼬리를 멤버가 아니라 상속을 받게할 수 있지만, 이는 튜플 요소가 생성자에서 초기화될 때 순서가 바뀌게 되어 문제가 발생할 수 있습니다. 이 문제는 기본 클래스 목록 내에 Tail에 앞서는 기본 클래스를 만들어 그 클래스 안에 head 멤버를 둔다면 해결할 수 있습니다. 하지만 이 방법도 동일한 형식이 두개면 요소를 추출하기 어렵기에 튜플의 높이를 미리 새겨 놓는 것도 방법이 됩니다.

template<typename... Types>
class Tuple;

template<unsigned Height, typename T>
class TupleElt {
  T value;
 public:
  TupleElt() = default;

  template<typename U>
  TupleElt(U&& other) : value(std::forward<U>(other)) { }

  T&       get()       { return value; }
  T const& get() const { return value; }
};

// recursive case:
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> 
 : private TupleElt<sizeof...(Tail), Head>, private Tuple<Tail...>
{
  using HeadElt = TupleElt<sizeof...(Tail), Head>;
 public:
  Head& getHead() { 
    return static_cast<HeadElt *>(this)->get();
  }
  Head const& getHead() const { 
    return static_cast<HeadElt const*>(this)->get();
  }
  Tuple<Tail...>& getTail() { return *this; }
  Tuple<Tail...> const& getTail() const { return *this; }
};

// basis case:
template<>
class Tuple<> {
  // no storage required
};

25.6 튜플 첨자

튜플의 요소에 접근하는 operator[]를 정의할 수 있지만, 튜플의 요소는 형식이 서로 다를 수 있기에 요소의 인덱스에 따라 결과형이 달라져야 합니다. 이는 CTValue를 통해 구현 가능합니다.

template<typename T, T Value>
struct CTValue {
  static constexpr T value = Value;
};


template<typename T, T Index>
auto& operator[](CTValue<T, Index>) {
  return get<Index>(*this);
};

'C++공부 > C++ Templates' 카테고리의 다른 글

27. 표현식 템플릿  (0) 2022.07.07
26. 구별 공용체  (0) 2022.07.07
24. 형식 목록  (0) 2022.07.06
23. 메타프로그래밍  (0) 2022.07.06
22. 정적과 동적 다형성 사이 잇기  (0) 2022.07.06

+ Recent posts