#!if 문서명2 != null
, [[C++/문법/템플릿]]
#!if 문서명3 != null
, [[C++/문법/템플릿 제약조건]]
#!if 문서명4 != null
, [[메타 데이터]]
#!if 문서명5 != null
, [[]]
#!if 문서명6 != null
, [[]]
프로그래밍 언어 문법 | |
{{{#!folding [ 펼치기 · 접기 ] {{{#!wiki style="margin: 0 -10px -5px; word-break: keep-all" | 프로그래밍 언어 문법 C(포인터 · 구조체 · size_t) · C++(이름공간 · 클래스 · 특성 · 상수 표현식 · 람다 표현식 · 템플릿/제약조건/메타 프로그래밍) · C# · Java · Python(함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript(표준 내장 객체, this) · Haskell(모나드) · 숨 |
마크업 언어 문법 HTML · CSS | |
개념과 용어 함수(인라인 함수 · 고차 함수 · 콜백 함수 · 람다식) · 리터럴 · 문자열 · 식별자(예약어) · 상속 · 예외 · 조건문 · 반복문 · 비트 연산 · 참조에 의한 호출 · eval · 네임스페이스 · 호이스팅 | |
기타 #! · == · === · deprecated · GOTO · NaN · null · undefined · 배커스-나우르 표기법 | }}}}}} |
프로그래밍 언어 목록 · 분류 · 문법 · 예제 |
1. 개요2. 튜링 완전3. 예제 1: 템플릿의 템플릿(1) - 특수화 여부를 확인하는 메타 함수4. 예제 2: 컴파일 시점 문자열5. 예제 3: 튜플 확장
5.1. 자료형 찾기
6. 예제 4: 직렬화 확장7. 예제 5: 자료형 리스트7.1. 관찰자7.2. 원소 접근7.3. 수정자
8. 예제 6: 템플릿의 템플릿 (2) - 메타 함수의 메타 함수9. 예제 7: 자료형 안전한 union 구현7.3.1. PushBack7.3.2. Append7.3.3. PopFront7.3.4. TryPopFront7.3.5. PopBack7.3.6. Set7.3.7. SwapElementAt7.3.8. Remove7.3.9. RemoveAll7.3.10. UniqueBy7.3.11. Unique7.3.12. InsertAt
7.4. GroupBy7.5. Union (합집합)7.6. Intersection (교집합)7.7. Difference (차집합)7.8. Select (순번 부분집합)1. 개요
C++의 메타 프로그래밍 개념 및 방법론을 설명하는 문서. 또한 예제를 겉들여 설명한다.1.1. 템플릿 (Template)
#!if (문단 == null) == (앵커 == null)
를
#!if 문단 != null & 앵커 == null
의 [[C++/문법/템플릿#s-|]]번 문단을
#!if 문단 == null & 앵커 != null
의 [[C++/문법/템플릿#|]] 부분을
참고하십시오.C++에서 메타 프로그래밍은 (아직까진) 템플릿의 도움이 필수적이다.
C++26
에 표준으로 모듈 <meta>
가 확정되었다. <meta>
의 코드 역시도 템플릿 매개변수를 통해 정적 리플렉션을 구현하므로 템플릿의 선제 학습이 필요하다.1.2. 템플릿 제약조건 (Constraints) & 개념 (Concepts)
#!if (문단 == null) == (앵커 == null)
를
#!if 문단 != null & 앵커 == null
의 [[C++/문법/템플릿 제약조건#s-|]]번 문단을
#!if 문단 == null & 앵커 != null
의 [[C++/문법/템플릿 제약조건#|]] 부분을
참고하십시오.#!if (문단 == null) == (앵커 == null)
를
#!if 문단 != null & 앵커 == null
의 [[C++/문법/템플릿 제약조건#s-|]]번 문단을
#!if 문단 == null & 앵커 != null
의 [[C++/문법/템플릿 제약조건#concept|concept]] 부분을
참고하십시오.의외로 메타 프로그래밍에 제약조건에 관한 지식은 별로 필요없다.
C++20
이전이었다면 SFINAE로만 이 문서를 꽉 채워야 했겠지만, 제약조건의 도입 이후 그저 템플릿 문서의 하위 문단일 뿐이다. 이 문서에서는 제약조건은 예제에서 옵션으로만 보이고 따로 설명은 하지 않겠다.2. 튜링 완전
이 문서에선 어떻게 템플릿 만으로 온전한 코드를 작성할 수 있는지 보인다. 런타임에 동작하는 코드와 템플릿 위에서 동작하는 코드가 어떤 차이가 있는지도 보이겠다.3. 예제 1: 템플릿의 템플릿(1) - 특수화 여부를 확인하는 메타 함수
우리는 표준 라이브러러리의 메타 함수, 제약조건을 쓰고 있었지만 이들도 결국 템플릿 외부의 값을 읽는데 도와주는 요소다. 참조형, 한정자, 바이트 크기 등. 자료형 자체의 특징을 검사하는 것은 아직도 지원이 부족하다. 그래서 그런지 템플릿이 특수화되었는지 아닌지 판별하는 메타 함수조차 표준에 없다. 다행히 우리가 직접 만들 수 있다.#!syntax cpp
template <typename T, typename U>
struct is_specialization : std::false_type {};
첫번째로 생각해볼만한 사항은 두 자료형을 받아 한쪽이 다른 쪽의 특수화인지 검사하는 것이다. 그전에 템플릿 특수화가 무엇인지 잠깐 짚고 넘어가자. 템플릿 특수화는 원본 템플릿의 템플릿 매개변수 일부를 실제, 실체화한 자료형으로 바꾸는 것이다. 그리고 명시한 자료형이 대입되는 상황에서 특별한 동작을 하도록 만드는 것이다. 상기 코드는 두개의 자료형을 받으며 기본적으로 false
를 가지고 있는 메타 함수다.#!syntax cpp
template<typename>
struct Struct {};
// (1) `Struct`의 특수화
Struct<int>;
// (2) 마찬가지로 `Struct`의 특수화
Struct<Struct<long>>;
특수화를 했을 때, `is_specialization<T, U>`
의 `T`
는 실체화가 된 자료형이, `U`
에는 원본 템플릿이 들어가야 한다. 상기 코드를 보면 특수화가 어떤 건지 확인할 수 있다.이 메타 함수는 특수화한 자료형을 받는 상황에선
true
를 반환하도록 특수화해야 한다. 헌데 구현하기 전에 인자를 넣어보자.#!syntax cpp
template <typename T, typename U>
struct is_specialization : std::false_type {};
template<typename>
struct Struct {};
// 컴파일 오류!
// 클래스 템플릿 `Struct`에 대한 인자가 없습니다!
is_specialization<Struct<int>, Struct>;
아직 구현을 하기도 전인데 오류가 뜬다. 이유가 무엇일까? 특수화는 실체화한(Instantiated) 자료형이 필요하다고 했었다. 그런데 템플릿이 실체화되려면 템플릿 매개변수가 모두 실체화한 자료형으로 채워져야 한다. 예시의 `Struct`
은 `Struct<int>`
마냥 매개변수를 명시하지 않으면 구현되지 않은 자료형이 되는 것이다! 가령 표준 라이브러리의 std::vector<T>
에 `T`
가 어떤 자료형인지 명시하지 않으면 사용하지 못하는 것과 같다.#!syntax cpp
template<typename T>
void Function(T arg);
template<template<typename D> typename T>
void TemplatedFunction(T arg);
어떤 자료형이 실체화되지 않았더라도 템플릿 매개변수에 넣을 수 있으려면, 애초에 템플릿의 매개변수가 실체화되지 않은 상태라도 담을 수 있어야 한다. 템플릿 매개변수의 typenme
은 그 자체로 하나의 온전한 자료형을 받는 것을 전제로 하기 때문이다. 그런데 실체화되지 않은 자료형이란 무엇일까? 템플릿 매개변수가 실체화되지 않았다는 말은 템플릿 매개변수도 어떤 템플릿으로 만들어야 한다는 것이다. 템플릿의 템플릿 (Template template) 매개변수는 template<>
안에 또다른 template<...> typename
을 넣어서 만들 수 있다.#!syntax cpp
template<typename T>
void Function(T arg);
template<template<typename D> typename T>
void TemplatedFunction();
// 상동
// 템플릿의 템플릿은 매개변수의 이름을 명시하지 않아도 됨!
template<template<typename> typename T>
void TemplatedFunction();
template<typename U>
struct Struct;
// (1) 템플릿 인자 연역 수행 (부패)
// 템플릿 자료형 매개변수 `T`가 숨겨짐. 굳이 명시하지 않아도 됨.
Function(1000);
// (2) `TemplatedFunction`의 `T`는 `Struct<typename U>`
// `Struct`에 템플릿 인자를 전달하지 않아도 됨.
TemplatedFunction<Struct>()
// (3) 컴파일 오류!
TemplatedFunction<int>();
// (4) 컴파일 오류!
Function(Struct<int>);
중요한 사실은 템플릿의 템플릿은 어떤 템플릿 매개변수에 들어올 때도 실체화가 되지 않으며, 자기가 갖고 있는 템플릿 매개변수 목록을 숨길 수 있다는 것이다. 이는 어디서 많이 본 상황이다. 우리가 C++에서 템플릿을 쓸 때 함수의 매개변수를 통해 템플릿의 매개변수를 숨길 수 있었다. 마찬가지로 템플릿 인자로 템플릿의 템플릿을 전달할 때 템플릿의 템플릿의 템플릿 매개변수들을 명시하지 않아도 된다. 복잡하긴 하지만, 지금까지 함수 템플릿, 클래스 템플릿 등을 선언하던 위치가 보통의 스코프가 아니라 template<>
안으로 옮겨졌다고 생각하면 된다. 템플릿 매개변수 목록도 하나의 스코프라고 생각하면 이해에 도움이 된다.#!syntax cpp
// `T`가 `U`의 특수화인지 검사하는 메타 함수
// `U`의 템플릿 매개변수는 이름을 명시하지 않아도 됨. 가변 템플릿이라도 마찬가지임.
template <typename T, template <typename...> typename U>
struct is_specialization : std::false_type {};
template<typename>
struct Struct {};
// 문제없음.
is_specialization<Struct<int>, Struct>;
// 컴파일 오류!
is_specialization<Struct<int>, int>;
처음 생각해볼 수 있는 건 앞서 작성했던 메타 함수에서 매개변수 하나를 템플릿의 템플릿으로 바꾸는 것이다. 이러면 앞서 경험한 오류가 해결된다. 하나 아쉬운 점은 템플릿이 아닌 자료형을 넣을 수 없다는 것인데, 이 메타 함수의 목적을 생각해보면 약간의 불편은 감수할만 하다. 상기 코드의 메타 함수는 기본값으로 false
를 가진다. 그리고 특수화가 된 상황에 대하여 특수화를 해주면 된다.한편 지금 동일한 용어가 중복되어 서술되는 것이 반복되는데 (이 문장조차도) 어쩔 수 없으나 다만 예제 위주로 읽어주기를 바란다.
#!syntax cpp
template <typename T, template <typename...> typename U>
struct is_specialization<???, U> : std::true_type {};
상기 코드는 특수화의 작성 도중 코드다. 아직 알지 못하는 것이, 첫번째 인자로 무엇을 넣을 것이냐다. 이럴 땐 특수화의 의미를 다시 떠올리자. 특수화는 템플릿 매개변수에 어떤 자료형이 들어온 것이라고 계속 언급했다. 그리고 `T`
에는 특수화가 완료된 자료형이 들어온다고 했다. 그러면 우리가 `T`
를 위해 매개변수를 직접 다 써주는 방법이 있겠다. 마침 우리는 `U`
로 원본 템플릿을 받아놨다. 즉 `T`
는 `U<typename...>`
의 매개변수를 명시한 꼴로 작성하면 된다.#!syntax cpp
template <template <typename...> typename U>
struct is_specialization<U<...>, U> : std::true_type {};
위와 같은 형태가 될 것이다. 잠깐 짚고 넘어가야 할 점은, `U`
가 가변 템플릿인 이유는 `U`
에 말 그대로 모든 종류의 템플릿을 넣기 위함이다. 우리는 여기서 템플릿 매개변수 묶음을 써서 `T`
자리에 들어갈 `U<...>`
에 매개변수를 명시해야 한다.#!syntax cpp
template <template <typename...> typename U, typename... Ts>
struct is_specialization<U<Ts...>, U> : std::true_type {};
위와 같이 작성했는데 보면 메타 함수 `is_specialization<T, U<...>>
의 `T`
가 `U<Ts...>`
로 바뀌었다. 결과적으로 이 메타 함수는 `U<Ts...>`
가 `U`
의 템플릿 특수화인지 검사하는 메타 함수며, 결과는 참이 맞다.#!syntax cpp
template <typename T, template <typename...> typename U>
struct is_specialization : std::false_type {};
template <template <typename...> typename Template, typename... Ts>
struct is_specialization<Template<Ts...>, Template> : std::true_type {};
template <typename T, template <typename...> typename Template>
constexpr bool is_specialization_v = is_specialization<T, Template>::value;
template <typename T, template <typename...> typename Template>
concept Specialized = is_specialization_v<T, Template>;
유틸리티 용 변수 템플릿까지 선언한 구현 내용은 위와 같다.#!syntax cpp
template<typename, typename>
struct Test {};
template<typename, typename>
struct Test2 {};
// (1) true
is_specialization_v<Test<int, int>, Test>;
// (2) false
is_specialization_v<Test<int, int>, Test2>;
// (3) true
is_specialization_v<Test<long&, int>, Test>;
// (4) true
// std::string은 std::basic_string<typename Char, typename CharTrait>의 특수화
is_specialization_v<std::string, std::basic_string>;
실행 예시는 위와 같다. 특수화한 경우를 잘 검사하는 걸 볼 수 있다.4. 예제 2: 컴파일 시점 문자열
#!syntax cpp
template<ConstexprString String>
struct Building;
template<"Hello, world!">
struct Building;
지금부터 구현할 것은 컴파일 시점에 내용이 결정되는 문자열이다. 당연히 템플릿 매개변수로서 사용할 수 있어야 한다. 지금까지 템플릿 매개변수에는 자료형과 숫자만 왔었는데, 이게 가능하다면 템플릿 괄호안에 문자열을 입력할 수 있게 된다. 시작하기 전에 몇가지 상황만 확인하고 넘어가자.#!syntax cpp
constexpr std::string ConstexprString{ "Hello, world!" };
먼저 C++20
부터 컴파일 시점에 힙 메모리 할당과 해제가 가능해졌다는 점이 있다. 덕분에 표준 문자열이 컴파일 시점에 값이 결정될 수 있다. 심지어 static_assert(bool, std::string{ "Error occured!" });
처럼 정적 어설션에도 사용할 수 있다. 하지만 컴파일러 지원 여부에 따라 될 수도 안될 수도 있다. 결정적으로 템플릿 매개변수는 자명한 자료형만 올 수 있기 때문에 std::string
은 사용할 수 없다. 따라서 우리가 직접 만들어야 한다.#!syntax cpp
template<typename Char, size_t L>
class BasicStaticString
{
public:
Char myData[L];
};
template<BasicStaticString String>
struct Building;
// 컴파일 오류!
Building<"Sungnyemun">;
constexpr BasicStaticString<char, 13> imFine{ "How are you?" };
먼저 정적인 정보를 가진 클래스 템플릿을 생각해볼 수 있다. 이 클래스는 집결 초기화(Aggregate Initialization)에 의해 내부 배열에 문자열을 잘 저장한다. 그러나 템플릿 매개변수에는 사용할 수 없다. 템플릿 매개변수 만으로는 클래스 템플릿의 템플릿 명세를 알아낼 수 없기 때문이다. 이러러면 추론 규칙을 직접 명시해야 한다.#!syntax cpp
template<typename TChar, size_t TLen>
BasicStaticString(TChar[TLen]) -> BasicStaticString<TChar, TLen>;
하지만 템플릿 연역 안내문(Template Deduction Guide)으로 자동 생성을 유도해도 안된다. 그런데 이때 오류 내용이 인수 목록이 일치하는 생성자가 없다고 뜬다. 왜냐하면 연역 안내문은 생성자를 필수로 요구하기 때문이다.#!syntax cpp
template<typename Char, size_t L>
class BasicStaticString
{
public:
template<typename Char, size_t TLen>
constexpr class BasicStaticString(const Char string[TLen])
{
for (size_t i = 0; i < TLen; ++i)
{
myData[i] = string[i];
}
}
Char myData[L];
};
template<typename TChar, size_t TLen>
BasicStaticString(const TChar string[TLen]) -> BasicStaticString<TChar, TLen>;
// `String`은 상수 템플릿 매개변수임.
template<BasicStaticString String>
struct Building;
// 컴파일 오류! 인수 목록이 일치하는 생성자가 없습니다.
Building<"Sungnyemun">;
하지만 아직 해결이 안된다. 왜냐하면 연역 안내는 부패되는 경우를 빼고 정확한 명세를 기입해야 한다. 그런데 const
와 [N]
만으론 자료형 범주를 다 기입한 게 아니고 참조자도 적어야 한다.#!syntax cpp
template<typename Char, size_t L>
class BasicStaticString
{
public:
template<typename Char, size_t TLen>
constexpr class BasicStaticString(const Char (&string)[TLen])
{
for (size_t i = 0; i < TLen; ++i)
{
myData[i] = string[i];
}
}
Char myData[L];
};
template<typename TChar, size_t TLen>
BasicStaticString(const TChar (&string)[TLen]) -> BasicStaticString<TChar, TLen>;
template<BasicStaticString String>
struct Building;
// 문제 없음.
Building<"Sungnyemun">;
// 이제 템플릿 매개변수를 명시하지 않아도 됨.
// BasicStaticString<wchar_t, 13>
constexpr BasicStaticString imFine{ L"How are you?" };
이제 잘 된다.- <C++ 예제 보기>
#!syntax cpp template<std::copyable Char, size_t L> class BasicStaticString { public: using char_type = Char; using type = BasicStaticString<Char, L>; static constexpr size_t Length = L; constexpr BasicStaticString() noexcept = default; template<std::copyable TChar, size_t TLen> constexpr BasicStaticString(const TChar (&string)[TLen]) noexcept(TLen <= Length) requires (TLen <= Length) { for (size_t i = 0; i < TLen; ++i) { myData[i] = string[i]; } } char_type myData[Length]{}; }; template<typename TChar, size_t TLen> BasicStaticString(const TChar (&string)[TLen]) -> BasicStaticString<TChar, TLen>; template<size_t L> using StaticString = BasicStaticString<char, L>; template<size_t L> using WideStaticString = BasicStaticString<wchar_t, L>;
5. 예제 3: 튜플 확장
템플릿 문서에서 구현한 튜플을 기능적으로 확장해보자.5.1. 자료형 찾기
#!syntax cpp
template<size_t Index, typename... Ts>
struct TupleTypeAtImpl;
template<size_t Index>
struct TupleTypeAtImpl<Index> {};
template<typename T, typename... Ts>
struct TupleTypeAtImpl<0, T, Ts...> { using Type = T; };
template<size_t Index, typename T, typename... Ts>
struct TupleTypeAtImpl<Index, T, Ts...> : public TupleTypeAtImpl<Index - 1, Ts...> {};
template<size_t Index, typename... Ts>
using TupleTypeAt = TupleTypeAtImpl<Index, Ts...>::Type;
int main()
{
// (1) 컴파일 오류! `Type0`이 존재하지 않습니다.
using Type0 = TupleTypeAt<0>;
// (2) `Type1`은 unsigned int
using Type1 = TupleTypeAt<0, unsigned>;
// (3) 컴파일 오류! `Type2`가 존재하지 않습니다.
using Type2 = TupleTypeAt<2, char, int>;
// (4) `Type3`은 int
using Type3 = TupleTypeAt<2, double, short, int>;
// (5) `Type4`는 short
using Type4 = TupleTypeAt<1, float, short, int, double>;
// (6) `Type5`는 void
using Type5 = TupleTypeAt<1, long, void>;
}
상기 코드는 SFINAE를 써서 특정 위치의 자료형을 얻는 예제다.6. 예제 4: 직렬화 확장
템플릿 문서에서 구현한 직렬화 함수를 확장해보자.7. 예제 5: 자료형 리스트
복수의 자료형을 담는 클래스 템플릿을 구현하고, 담긴 자료형을 조작하는 법을 알아보자.#!syntax cpp
template<typename... Ts>
struct TypeList
{
explicit TypeList() noexcept = default;
};
기본적인 리스트 구조체는 위와 같을 것이다. 가변 템플릿으로 여러 개의 자료형을 받고, 그게 전부다. 대신 리스트가 가질 수 있는 (메타) 함수를 한번 생각해보자. 비었는지 확인, 길이, 갖고 있는지 확인, 특정 위치의 원소, 첫번째와 마지막 원소, 동등 비교 등등.우리는 이 구조체 내용에 대해서는 볼 것이 없고, 템플릿 위주로 구현하고자 한다.#!syntax cpp
struct MetaOutOfIndexError { explicit MetaOutOfIndexError() noexcept = default; };
struct MetaNotFoundError { explicit MetaNotFoundError() noexcept = default; };
메타 함수를 위한 유틸리티도 정의하겠다. 몇몇 메타 함수는 컴파일 오류 대신 이러한 자료형을 결과로 반환한다.7.1. 관찰자
7.1.1. IsEmpty
#!syntax cpp
template<typename TList>
struct IsEmpty;
template<typename... Ts>
struct IsEmpty<TypeList<Ts...>> : std::false_type {};
template<>
struct IsEmpty<TypeList<>> : std::true_type {};
비었는지 확인하는 메타 함수는 위와 같이 특수화하면 구현할 수 있다. C++/표준 라이브러리의 std::true_type
과 std::false_type
을 상속받고 있다. 이 구조체들은 bool
의 정적 데이터 멤버 `value`
를 가지고 있다.자료형 리스트가 매개변수로 들어오지 않은 경우는 아예 구현하지 않았다. 이유는 오로지 자료형 리스트만 받고자 함이며 다른 자료형을 쓰면 컴파일 오류를 띄우기 위함이다. 그러나 코드에 실제로 쓰이는 때에만 오류를 띄우므로 바로 알 수가 없다. 이럴 때는 개념을 쓰자.
#!syntax cpp
// (1) C++11
template<typename TList>
struct IsEmpty
{
static_assert(false, "Do not instantiate the original template of 'IsEmpty'!");
};
// (2) C++20
template<typename TList>
requires (is_specialized_v<TList, TypeList>)
struct IsEmpty
{};
상기 코드에선 원본 메타 함수를 쓰면 컴파일 오류를 띄우도록 했다.#!syntax cpp
template<typename TList>
constexpr bool IsEmpty_v = IsEmpty<TList>::value;
값을 읽는 상수 템플릿도 선언하자. 이런 식으로 메타 함수를 만들면 가독성을 상당히 개선할 수 있다.- <C++ 예제 보기>
#!syntax cpp // (1) true IsEmpty_v<TypeList<>>; // (2) false IsEmpty_v<TypeList<double>>; // (3) false IsEmpty_v<TypeList<std::string, int, long long>>; // (4) false IsEmpty_v<TypeList<TypeList<>, TypeList<>>>;
`TypeList`
는 또다른 `TypeList`
를 템플릿 매개변수로 가질 수 있다. 이후에 템플릿의 템플릿과 연계한 활용법을 설명한다.7.1.2. GetLength
#!syntax cpp
template<typename TList>
struct GetLength;
template<>
struct GetLength<TypeList<>> : std::integral_constant<size_t, 0> {};
template<typename... Ts>
struct GetLength<TypeList<Ts...>> : std::integral_constant<size_t, sizeof...(Ts)> {};
리스트의 길이를 구하는 메타 함수는 위와 같이 구현할 수 있다.#!syntax cpp
template<typename TList>
constexpr size_t GetLength_v = GetLength<TList>::value;
값을 읽는 상수 템플릿은 위와 같다.7.1.3. GetByteSize
#!syntax cpp
template<typename TList>
struct GetByteSize;
template<>
struct GetByteSize<TypeList<>> : std::integral_constant<size_t, 0> {};
template<typename... Ts>
struct GetByteSize<TypeList<Ts...>> : std::integral_constant<size_t, sizeof(Ts) + ...> {};
바이트 크기의 합을 구하는 메타 함수는 위와 같이 구현할 수 있다.#!syntax cpp
template<typename TList>
constexpr size_t GetByteSize_v = GetByteSize<TList>::value;
값을 읽는 상수 템플릿은 위와 같다.7.1.4. Has
#!syntax cpp
template<typename U, typename TList>
struct Has;
template<typename U>
struct Has<U, TypeList<>> : std::false_type {};
template<typename U, typename... Ts>
struct Has<U, TypeList<Ts...>> : std::disjunction<std::is_same<U, Ts>...> {};
특정 자료형이 리스트 안에 속해있는지 검사하는 메타 함수는 위와 같이 구현할 수 있다. C++/표준 라이브러리의 std::disjunction<Ts...>
을 사용했다. std::disjunction<Ts...>
은 인자로 받은 자료형들에서 bool value
를 받아 OR 연산을 실행한다.#!syntax cpp
template<typename TList, typename U>
constexpr bool Has_v = Has<U, TList>::value;
값을 읽는 상수 템플릿은 위와 같다.- <C++ 예제 보기>
#!syntax cpp // (1) false Has_v<TypeList<>, bool>; // (2) true Has_v<TypeList<double, int, unsigned, long, float>, long>; // (3) false Has_v<TypeList<int, short, char, long long>, short&>; // (4) false Has_v<TypeList<>, void>; // (5) true Has_v<TypeList<TypeList<int>, TypeList<unsigned char>, TypeList<int>>, TypeList<int>>;
7.1.5. EqualTo, NotEqualTo
#!syntax cpp
template<typename TListLhs, typename TListRhs>
struct EqualTo;
template<typename... Ts, typename... Us>
struct EqualTo<TypeList<Ts...>, TypeList<Us...>> : std::is_same<TypeList<Ts...>, TypeList<Us...>> {};
template<typename TListLhs, typename TListRhs>
constexpr bool EqualTo_v = EqualTo<TListLhs, TListRhs>::value;
template<typename TListLhs, typename TListRhs>
constexpr bool NotEqualTo_v = !EqualTo_v<TListLhs, TListRhs>;
두 리스트를 동등 비교하는 메타 함수는 위와 같이 구현할 수 있다.7.2. 원소 접근
7.2.1. At
#!syntax cpp
template<size_t I, typename TList>
struct At;
template<typename Indexer, typename TList>
struct AtImpl;
template<typename T, typename T, typename... Ts>
struct AtImpl<std::index_sequence<>, TypeList<T, Ts...>>
{
using type = T;
};
template<size_t I, typename T, size_t... Indices, typename... Ts>
struct AtImpl<std::index_sequence<I, Indices...>, TypeList<T, Ts...>> : AtImpl<std::index_sequence<Indices...>, TypeList<Ts...>> {};
template<size_t I, typename... Ts>
requires (I < sizeof...(Ts))
struct At<I, TypeList<Ts...>> : AtImpl<std::make_index_sequence<I>, TypeList<Ts...>> {};
특정 순서의 자료형을 구하는 메타 함수는 위와 같이 구현할 수 있다.#!syntax cpp
template<typename TList, size_t I>
using At_t = At<I, TList>::type;
자료형을 읽는 자료형 별칭 템플릿은 위와 같다.7.2.2. AtFirst
#!syntax cpp
template<typename TList>
struct AtFirst;
template<>
struct AtFirst<TypeList<>> {};
template<typename T, typename... Ts>
struct AtFirst<TypeList<T, Ts...>>
{
using type = T;
};
template<typename TList>
using AtFirst_t = AtFirst<TList>::type;
첫번째 자료형 원소를 구하는 메타 함수는 위와 같이 구현할 수 있다.7.2.3. AtLast
#!syntax cpp
template<typename TList>
struct AtLast;
template<>
struct AtLast<TypeList<>> {};
template<typename T>
struct AtLast<TypeList<T>>
{
using type = T;
};
template<typename... Ts>
struct AtLast<TypeList<Ts...>> : At<sizeof...(Ts) - 1, TypeList<Ts...>> {};
template<typename TList>
using AtLast_t = AtLast<TList>::type;
마지막 자료형 원소를 구하는 메타 함수는 위와 같이 구현할 수 있다.7.2.4. FirstIndexOf
#!syntax cpp
template<typename U, typename TList>
struct FirstIndexOf;
template<typename U, typename TList, typename Indexer>
struct FirstIndexOfImpl;
template<typename U, typename... Ts, size_t I, size_t... Indices>
struct FirstIndexOfImpl<U, TypeList<U, Ts...>, std::index_sequence<I, Indices...>>
: std::integral_constant<size_t, I>
{};
template<typename U, typename T, typename... Ts, size_t I, size_t... Indices>
struct FirstIndexOfImpl<U, TypeList<T, Ts...>, std::index_sequence<I, Indices...>>
: FirstIndexOfImpl<U, TypeList<Ts...>, std::index_sequence<Indices...>>
{};
template<typename U, typename... Ts> requires (Has_v<TypeList<Ts...>, U>)
struct FirstIndexOf<U, TypeList<Ts...>>
: FirstIndexOfImpl<U, TypeList<Ts...>, std::index_sequence_for<Ts...>>
{
using result = U;
};
template<typename U, typename... Ts> requires (!Has_v<TypeList<Ts...>, U>)
struct FirstIndexOf<U, TypeList<Ts...>>
{
using result = MetaNotFoundError;
};
template<typename TList, typename U>
using FirstIndexOf_t = FirstIndexOf<U, TList>::result;
template<typename TList, typename U> requires (Has_v<TList, U>)
constexpr size_t FirstIndexOf_v = FirstIndexOf<U, TList>::value;
특정 종류 원소의 첫번째 순번을 구하는 메타 함수는 위와 같이 구현할 수 있다. 이때 원소를 찾지 못하면 MetaNotFoundError
을 멤버로 갖는다.7.3. 수정자
7.3.1. PushBack
#!syntax cpp
template<typename TList, typename... Args>
struct PushBack;
template<typename... Ts, typename... Us>
struct PushBack<TypeList<Ts...>, Us...>
{
using type = TypeList<Ts..., Us...>;
};
template<typename TList, typename... Args>
using PushBack_t = PushBack<TList, Args...>::type;
한 리스트 뒤에 다른 원소를 덧붙이는 메타 함수는 위와 같이 구현할 수 있다.7.3.2. Append
#!syntax cpp
template<typename TList, typename... Args>
struct Append<TList, Args...>;
template<typename... Ts>
struct Append<TypeList<Ts...>>
{
using type = TypeList<Ts...>;
};
template<typename... Ts, typename... Us>
struct Append<TypeList<Ts...>, TypeList<Us...>>
{
using type = TypeList<Ts..., Us...>;
};
template<typename... Ts, typename... Us, typename... Vs>
struct Append<TypeList<Ts...>, TypeList<Us...>, TypeList<Vs...>>
{
using type = TypeList<Ts..., Us..., Vs...>;
};
template<typename... Ts, typename... Us, typename... Rests>
struct Append<TypeList<Ts...>, TypeList<Us...>, Rests...>
{
using type = Append<TypeList<Ts..., Us...>, Rests...>;
};
template<typename TList, typename... Args>
using Append_t = Append<TList, Args...>::type;
한 리스트 뒤에 다른 리스트를 포함한 다수의 원소를 합치는 메타 함수는 위와 같이 구현할 수 있다.7.3.3. PopFront
#!syntax cpp
template<typename TList>
struct PopFront;
template<>
struct PopFront<TypeList<>> {};
template<typename T, typename... Ts>
struct PopFront<TypeList<T, Ts...>>
{
using type = TypeList<Ts...>;
using result = T;
};
template<typename TList>
using PopFront_t = PopFront<TList>::type;
template<typename TList>
using PopFront_r = PopFront<TList>::result;
리스트 앞에서 원소 하나를 빼는 메타 함수는 위와 같이 구현할 수 있다.7.3.4. TryPopFront
#!syntax cpp
template<typename TList>
struct TryPopFront;
template<>
struct TryPopFront<TypeList<>>
{
using type = TypeList<>;
using result = MetaNotFoundError;
};
template<typename T, typename... Ts>
struct TryPopFront<TypeList<T, Ts...>>
{
using type = TypeList<Ts...>;
using result = T;
};
template<typename TList>
using TryPopFront_t = TryPopFront<TList>::type;
template<typename TList>
using TryPopFront_r = TryPopFront<TList>::result;
시도 버전은 상기 코드와 같다.7.3.5. PopBack
#!syntax cpp
template<typename TList>
struct PopBack;
template<>
struct PopBack<TypeList<>> {};
template<typename TList, typename Backup, size_t J>
struct PopBackImpl;
template<typename T, typename Backup, size_t J>
struct PopBackImpl<TypeList<T>, Backup, J>
{
using type = Backup;
using result = T;
};
template<typename T, typename Backup, size_t J, typename... Ts>
struct PopBackImpl<TypeList<T, Ts...>, Backup, J> : PopBackImpl<TypeList<Ts...>, PushBack_t<Backup, T>, J - 1>
{};
template<typename... Ts>
struct PopBack<TypeList<Ts...>> : PopBackImpl<TypeList<Ts...>, TypeList<>, sizeof...(Ts) - 1>
{};
template<typename TList>
using PopBack_t = PopBack<TList>::type;
template<typename TList>
using PopBack_r = PopBack<TList>::result;
리스트 뒤에서 원소 하나를 빼는 메타 함수는 위와 같이 구현할 수 있다.7.3.6. Set
#!syntax cpp
template<size_t I, typename U, typename TList>
struct Set;
template<size_t I, typename U>
struct Set<I, U, TypeList<>>
{
static_assert(false);
using type = MetaOutOfIndexError;
};
template<size_t I, typename U, typename... Ts> requires (sizeof...(Ts) <= I)
struct Set<I, U, TypeList<Ts...>>
{
static_assert(false);
using type = MetaOutOfIndexError;
};
template<size_t I, typename U, typename TList, typename Front>
struct SetImpl;
template<typename U, typename Front, typename T, typename... Ts>
struct SetImpl<0, U, TypeList<T, Ts...>, Front>
{
using type = Append_t<PushBack_t<Front, U>, TypeList<Ts...>>
};
template<size_t I, typename U, typename Front, typename T, typename... Ts>
struct SetImpl<I, U, TypeList<T, Ts...>, Front> : SetImpl<I - 1, U, TypeList<Ts...>, PushBack_t<Front, T>>
{};
template<size_t I, typename U, typename... Ts>
struct Set<I, U, TypeList<Ts...>> : SetImpl<I, U, TypeList<Ts...>, TypeList<>>
{};
template<typename TList, size_t I, typename U>
using Set_t = Set<I, U, TList>::type;
특정 순번의 원소를 설정하는 메타 함수는 위와 같이 구현할 수 있다.7.3.7. SwapElementAt
#!syntax cpp
template<size_t L, size_t R, typename TList>
struct SwapElementAt;
template<size_t L, size_t R, typename... Ts>
struct SwapElementAt<L, R, TypeList<Ts...>>
{
private:
using _Lparam = At_t<TypeList<Ts...>, L>;
using _Rparam = At_t<TypeList<Ts...>, R>;
public:
using type = Set_t<Set_t<TypeList<Ts...>, R, _Lparam>, L, _Rparam>;
};
template<typename TList, size_t L, size_t R>
using SwapElementAt_t = SwapElementAt<L, R, TList>::type;
두 순번에 자리한 원소를 뒤바꾸는 메타 함수는 위와 같이 구현할 수 있다.7.3.8. Remove
#!syntax cpp
template<typename U, typename TList>
struct Remove;
template<typename U>
struct Remove<U, TypeList<>> : std::false_type
{
using type = TypeList<>;
};
template<typename U, typename TList, typename Result>
struct RemoveImpl;
template<typename U, typename Result>
struct RemoveImpl<U, TypeList<>, Result> : std::false_type
{
using type = Result;
};
template<typename Result>
struct RemoveImpl2 : std::true_type
{
using type = Result;
};
template<typename U, typename T, typename... Ts, typename Result>
struct RemoveImpl<U, TypeList<T, Ts...>, Result>
: std::conditional_t<std::is_same_v<U, T>, RemoveImpl2<PushBack_t<Result, Ts...>>, RemoveImpl<U, TypeList<Ts...>, PushBack_t<Result, T>>>
{};
template<typename U, typename... Ts>
struct Remove<U, TypeList<Ts...>> : RemoveImpl<U, TypeList<Ts...>, TypeList<>>
{};
template<typename TList, typename U>
using Remove_t = Remove<U, TList>::type;
template<typename TList, typename U>
constexpr bool Remove_v = Remove<U, TList>::value;
리스트의 앞쪽부터 특정 원소를 찾아 제거하는 메타 함수는 위와 같이 구현할 수 있다.#!syntax cpp
using tlist0 = TypeList<>;
using tlist1 = TypeList<bool, char, short, int, long>;
using tlist2 = TypeList<int, int, int, float, float>;
// (1) TypeList<>
Remove_t<tlist0, short>;
// (2) false
Remove_v<tlist0, short>;
// (3) TypeList<bool, char, int, long>
Remove_t<tlist1, short>;
// (4) true
Remove_v<tlist1, short>;
// (5) TypeList<bool, char, short, int, long>
Remove_t<tlist1, float>;
// (6) false
Remove_v<tlist1, float>;
// (7) TypeList<int, int, float, float>
Remove_t<tlist2, int>;
// (8) true
Remove_v<tlist2, int>;
실행한 결과는 위와 같다.7.3.9. RemoveAll
#!syntax cpp
template<typename U, typename TList>
struct RemoveAll;
template<typename U, typename TList, typename Result, size_t Count>
struct RemoveAllImpl;
template<typename U, typename Result, size_t Count>
struct RemoveAllImpl<U, TypeList<>, Result, Count> : std::integral_constant<size_t, Count>
{
using type = Result;
};
template<typename U, typename Result, size_t Count, typename T, typename... Ts>
struct RemoveAllImpl<U, TypeList<T, Ts...>, Result, Count>
: RemoveAllImpl<U, TypeList<Ts...>, std::conditional_t<std::is_same_v<U, T>, Result, PushBack_t<Result, T>>, std::is_same_v<U, T> ? Count + 1 : Count>
{};
template<typename U, typename... Ts>
struct RemoveAll<U, TypeList<Ts...>> : RemoveAllImpl<U, TypeList<Ts...>, TypeList<>, 0>
{};
template<typename TList, typename U>
using RemoveAll_t = RemoveAll<U, TList>::type;
template<typename TList, typename U>
constexpr size_t RemoveAll_v = RemoveAll<U, TList>::value;
리스트에서 특정 원소를 찾아 전부 제거하는 메타 함수는 위와 같이 구현할 수 있다.7.3.10. UniqueBy
#!syntax cpp
template<typename U, typename TList>
struct UniqueBy;
template<typename U>
struct UniqueBy<U, TypeList<>> : std::false_type
{
using type = TypeList<>;
};
template<typename U, typename Result, typename Backup, bool IsFirst>
struct UniqueByImpl1;
template<typename U, typename Backup>
struct UniqueByImpl1<U, TypeList<>, Backup, false> : std::true_type
{
using type = Backup;
};
template<typename U, typename Backup>
struct UniqueByImpl1<U, TypeList<>, Backup, true> : std::false_type
{
using type = Backup;
};
template<typename U, typename T, typename Backup, typename... Ts>
struct UniqueByImpl1<U, TypeList<T, Ts...>, Backup, false>
: std::conditional_t<std::is_same_v<U, T>
, UniqueByImpl1<U, TypeList<Ts...>, Backup, false>
, UniqueByImpl1<U, TypeList<Ts...>, PushBack_t<Backup, T>, false>>
{};
template<typename U, typename T, typename Backup, typename... Ts>
struct UniqueByImpl1<U, TypeList<T, Ts...>, Backup, true>
: UniqueByImpl1<U, TypeList<Ts...>, PushBack_t<Backup, T>, !std::is_same_v<U, T>>
{};
template<typename Result>
struct UniqueByImpl2 : std::false_type
{
using type = Result;
};
template<typename U, typename... Ts>
struct UniqueBy<U, TypeList<Ts...>>
: std::conditional_t<(std::is_same_v<U, Ts> || ...)
, UniqueByImpl1<U, TypeList<Ts...>, TypeList<>, true>
, UniqueByImpl2<TypeList<Ts...>>>
{};
template<typename TList, typename U>
using UniqueBy_t = UniqueBy<U, TList>::type;
template<typename TList, typename U>
constexpr bool UniqueBy_v = UniqueBy<U, TList>::value;
리스트에서 중복되는 원소를 한번 제거하는 메타 함수는 위와 같이 구현할 수 있다.7.3.11. Unique
#!syntax cpp
template<typename TList, typename Origin>
struct UniqueImpl1;
template<typename Result>
struct UniqueImpl2
{
using type = Result;
};
template<typename U // 현재 순회자 자료형, 삭제 대상
, typename Current // 현재 순회 중인 리스트
, typename Result // 결과 (처음엔 빈 리스트)
, typename Origin> // 원본 리스트
struct UniqueImpl3;
template<typename Origin>
struct UniqueImpl1<TypeList<>, Origin> : UniqueImpl2<Origin> {};
template<typename T, typename Origin>
struct UniqueImpl1<TypeList<T>, Origin> : UniqueImpl2<Origin> {};
// 전부 다 같은 원소일 경우
template<typename T, typename Origin, typename... Ts> requires (std::is_same_v<T, Ts> && ...)
struct UniqueImpl1<TypeList<T, Ts...>, Origin> : UniqueImpl2<TypeList<T>> {};
// 순회를 완료한 경우
template<typename U, typename Result, typename Origin>
struct UniqueImpl3<U, TypeList<>, Result, Origin> : UniqueImpl2<Result> {};
// 전부 다 같은 원소일 경우
template<typename U
, typename T
, typename Result
, typename Origin
, typename... Ts> requires (std::is_same_v<T, Ts> && ...)
struct UniqueImpl3<U, TypeList<T, Ts...>, Result, Origin> : UniqueImpl2<PushBack_t<Result, T>>
{};
template<typename U
, typename T
, typename Result
, typename Origin
, typename... Ts>
struct UniqueImpl3<U, TypeList<T, Ts...>, Result, Origin>
: UniqueImpl3<AtFirst_t<RemoveAll_t<TypeList<T, Ts...>, U>>
, RemoveAll_t<TypeList<T, Ts...>, U>
, PushBack_t<Result, U>
, Origin>
{};
template<typename Origin, typename U>
struct UniqueValidationIterator
: std::negation<std::is_same<UniqueBy_t<Origin, U>, Origin>> {};
template<typename T, typename... Ts, typename Origin>
struct UniqueImpl1<TypeList<T, Ts...>, Origin>
: std::conditional_t
<
std::disjunction_v
<
UniqueValidationIterator<Origin, T>, UniqueValidationIterator<Origin, Ts>...
>
, UniqueImpl3<T, TypeList<T, Ts...>, TypeList<>, TypeList<T, Ts...>>
, UniqueImpl2<TypeList<T, Ts...>>
>
{};
template<typename TList>
struct Unique;
template<typename... Ts>
struct Unique<TypeList<Ts...>> : UniqueImpl1<TypeList<Ts...>, TypeList<Ts...>> {};
template<typename TList>
using Unique_t = Unique<TList>::type;
리스트에서 중복되는 원소를 전부 제거하는 메타 함수는 위와 같이 구현할 수 있다. C++/표준 라이브러리의 std::negation<T>
을 사용했다. std::negation<T>
은 인자로 받은 자료형 `T`
의 bool value
값에 NOT 연산을 실행한다.7.3.12. InsertAt
#!syntax cpp
template<typename U, size_t Index, typename TList>
struct InsertAt;
template<typename U>
struct InsertAt<U, 0, TypeList<>>
{
using type = TypeList<U>;
};
template<typename U, size_t Index>
struct InsertAt<U, Index, TypeList<>>
{
static_assert(false);
using type = MetaOutOfIndexError;
};
template<typename U, size_t Index, typename... Ts>
struct InsertAt<U, Index, TypeList<Ts...>>
{
static_assert(false);
using type = MetaOutOfIndexError;
};
template<typename U, size_t Index, typename TList, typename Front, typename Indexer>
struct InsertAtImpl;
template<typename U, size_t Index, typename Front, typename... Ts>
struct InsertAtImpl<U, Index, TypeList<Ts...>, Front, std::index_sequence<>>
{
using type = Append_t<Front, TypeList<U, Ts...>>;
};
template<typename U, size_t Index
, typename Front
, typename T, typename... Ts
, size_t I, size_t... Indices>
struct InsertAtImpl<U, Index, TypeList<T, Ts...>, Front, std::index_sequence<I, Indices...>>
: InsertAtImpl<U, Index, TypeList<Ts...>, PushBack_t<Front, T>, std::index_sequence<Indices...>>
{};
template<typename U, size_t Index, typename... Ts>
requires (Index <= sizeof...(Ts))
struct InsertAt<U, Index, TypeList<Ts...>>
: InsertAtImpl<U, Index, TypeList<Ts...>, TypeList<>, std::make_index_sequence<Index>>
{};
template<typename TList, size_t Index, typename U>
using InsertAt_t = InsertAt<U, Index, TList>::type;
리스트의 특정 위치에 원소를 삽입하는 메타 함수는 위와 같이 구현할 수 있다. 이때 지정한 번호가 리스트의 크기 초과이면 MetaOutOfIndexError
를 멤버로 갖는다.7.4. GroupBy
#!syntax cpp
template<typename U, typename TList>
struct GroupBy;
template<typename U, typename TList, typename Result, typename Current, typename Rests>
struct GroupByImpl;
template<typename U, typename Result, typename Current, typename Rests>
struct GroupByImpl<U, TypeList<>, Result, Current, Rests>
{
using type = Append_t<Current, Rests>;
};
template<typename U
, typename Result
, typename Current
, typename Rests
, typename T, typename... Ts>
struct GroupByImpl<U, TypeList<T, Ts...>, Result, Current, Rests>
: std::conditional_t<std::is_same_v<U, T>
, GroupByImpl<U, TypeList<Ts...>, Result, PushBack_t<Current, T>, Rests>
, GroupByImpl<U, TypeList<Ts...>, PushBack_t<Result, T>, Current, PushBack_t<Rests, T>>>
{};
template<typename U>
struct GroupBy<U, TypeList<>>
{
using type = TypeList<>;
};
template<typename U, typename T>
struct GroupBy<U, TypeList<T>>
{
using type = TypeList<T>;
};
template<typename U, typename T, typename... Ts> requires (std::is_same_v<T, Ts> && ...)
struct GroupBy<U, TypeList<T, Ts...>>
{
using type = TypeList<T, Ts...>;
};
template<typename U, typename... Ts>
struct GroupBy<U, TypeList<Ts...>>
: GroupByImpl<U, TypeList<Ts...>, TypeList<>, TypeList<>, TypeList<>>
{};
template<typename TList, typename U>
using GroupBy_t = GroupBy<U, TList>::type;
같은 원소를 순서를 바꿔 한데 모으는 메타 함수는 위와 같이 구현할 수 있다. 참고로 이 구현에선 지정한 원소들이 리스트 앞으로 모인다.#!syntax cpp
// (1) TypeList<long, long, float>
GroupBy_t<TypeList<long, float, long>, long>;
// (2) TypeList<int, int, int, long, double, const short, bool, unsigned long long, float, short>
GroupBy_t<TypeList<long, int, double, const short, bool, unsigned long long, int, int, float, short>, int>;
실행한 결과는 위와 같다.7.5. Union (합집합)
#!syntax cpp
template<typename TListLhs, typename TListRhs>
struct Union;
template<typename... Ts>
struct Union<TypeList<Ts...>, TypeList<>>
{
using type = TypeList<Ts...>;
};
template<typename... Ts>
struct Union<TypeList<>, TypeList<Ts...>>
{
using type = TypeList<Ts...>;
};
template<typename... Ts>
struct Union<TypeList<Ts...>, TypeList<Ts...>>
{
using type = TypeList<Ts...>;
};
template<typename... Ts, typename... Us>
struct Union<TypeList<Ts...>, TypeList<Us...>>
{
using type = Unique_t<TypeList<Ts..., Us...>>;
};
template<typename TListLhs, typename TListRhs>
using Union_t = Union<TListLhs, TListRhs>::type;
두 리스트에서 중복되는 원소를 제한 합집합을 구하는 메타 함수는 위와같이 구현할 수 있다.7.6. Intersection (교집합)
#!syntax cpp
template<typename TListLhs, typename TListRhs>
struct Intersection;
template<typename... Ts>
struct Intersection<TypeList<Ts...>, TypeList<Ts...>>
{
using type = TypeList<Ts...>;
};
template<typename TListLhs, typename TListRhs, typename Result>
struct IntersectionImpl;
template<typename Result, typename... Ts>
struct IntersectionImpl<TypeList<Ts...>, TypeList<>, Result>
{
using type = Result;
};
template<typename Result, typename... Ts>
struct IntersectionImpl<TypeList<>, TypeList<Ts...>, Result>
{
using type = Result;
};
template<typename T, typename TList, typename UList>
struct IntersectionIterator : std::conjunction<Has<T, TList>, Has<T, UList>> {};
template<typename Result, typename T, typename... Ts, typename... Us>
struct IntersectionImpl<TypeList<T, Ts...>, TypeList<Us...>, Result>
: std::conditional_t<Has_v<TypeList<Us...>, T>
, IntersectionImpl<TypeList<Ts...>, Remove_t<TypeList<Us...>, T>, PushBack_t<Result, T>>
, IntersectionImpl<TypeList<Ts...>, TypeList<Us...>, Result>>
{};
template<typename... Ts, typename... Us>
struct Intersection<TypeList<Ts...>, TypeList<Us...>>
: IntersectionImpl<TypeList<Ts...>, TypeList<Us...>, TypeList<>>
{};
template<typename TListLhs, typename TListRhs>
using Intersection_t = Intersection<TListLhs, TListRhs>::type;
두 리스트의 교집합을 구하는 메타 함수는 위와 같이 구현할 수 있다.7.7. Difference (차집합)
#!syntax cpp
template<typename TListLhs, typename TListRhs>
struct Difference;
template<typename... Ts>
struct Difference<TypeList<Ts...>, TypeList<Ts...>>
{
using type = TypeList<>;
};
template<typename... Ts>
struct Difference<TypeList<Ts...>, TypeList<>>
{
using type = TypeList<Ts...>;
};
template<typename U, typename TList, typename Result>
struct DifferenceImpl;
template<typename U, typename Result>
struct DifferenceImpl<U, TypeList<>, Result>
{
using type = Result;
};
template<typename U, typename V, typename Result>
struct DifferenceImpl<U, TypeList<V>, Result>
{
using type = RemoveAll_t<Result, V>;
};
template<typename U, typename Result, typename V, typename...Vs>
struct DifferenceImpl<U, TypeList<V, Vs...>, Result> : DifferenceImpl<V, TypeList<Vs...>, RemoveAll_t<Result, U>>
{};
template<typename U, typename... Ts, typename... Us>
struct Difference<TypeList<Ts...>, TypeList<U, Us...>> : DifferenceImpl<U, TypeList<Us...>, TypeList<Ts...>> {};
template<typename TListLhs, typename TListRhs>
using Difference_t = Difference<TListLhs, TListRhs>::type;
왼쪽 리스트에서 오른쪽 리스트를 빼는 메타 함수는 위와 같이 구현할 수 있다.7.8. Select (순번 부분집합)
#!syntax cpp
template<size_t... Indices, typename TList>
struct Select;
리스트에서 특정 순번의 원소를 모은 리스트를 구하는 메타 함수.8. 예제 6: 템플릿의 템플릿 (2) - 메타 함수의 메타 함수
메타 프로그래밍의 꽃은 프로그램 자기 자신조차 한낱 객체로 취급하는 것이다. 앞서 구현한 자료형 리스트를 질적으로 확장해보자.8.1. Invoke
#!syntax cpp
template<typename Functor, typename... Args>
using Invoke = Functor::template Invoke<Args...>;
template<typename Functor, typename... Args>
using Invoke_t = Invoke<Functor, Args...>;
Invoke
는 일종의 메타 함수 오버로딩이다. 당연히 템플릿의 기능 확장을 위해서는 특수화를 해주는 것이 컴파일 속도나 기능 면에서는 훨 좋다. 그러나 실제로 작성할 때는 코드의 가독성이 무너져버려서 아주 읽기 싫은 코드가 되버린다. 한번 쓰고 다시는 안 읽는 코드가 되기 십상이다. 그래서 최대한 범용적인 유틸리티를 만들어서 가능한 한 모든 메타 함수에 적용할 수 있게끔 하자는 거다.그래서 몇가지 규칙이 필요하다. 메타 함수
`Functor`
를 실행시키는 메타 함수를 만들려고 한다.`Functor`
는 멤버 자료형 (별칭) 템플릿으로 `Invoke<typename...>`
을 가지고 있어야 하는 게 첫번째 규칙이다. 그리고 `Invoke<typename...>`
가 반환한 (다시 말해서 실행한) 결과물도 어떤 자료형이어야 한다. 예를 들어서 우리가 앞서 구현했던 메타 함수들이 반환될 수 있다.말로 하면 너무 어렵고 예제를 봐야 한다.
8.2. MakeCallable
#!syntax cpp
template<template <typename...> typename Functor>
struct MakeCallable
{
template <class... Params>
requires (is_specialization_v<Functor<Params...>, Functor>)
using Invoke = Functor<Params...>;
};
아무 템플릿이나 위의 `Invoke`
로 실행시킬 수 있는 메타 함수로 만드는 메타 함수다. 즉 어떤 클래스 템플릿을 받아서 멤버로 `Invoke`
를 추가해주는 Wrapper의 기능을 한다. 자료형 리스트 말고 모든 유형의 템플릿에 사용할 수 있다.#!syntax cpp
// (1) 문제 없음.
using call0 = MakeCallable<std::is_same>;
// call0::Invoke == std::is_same
// call0::Invoke<int, int> == std::is_same<int, int>
call0::Invoke<int, int>;
// 상동
Invoke<call0, int, int>;
// 제약조건 존재
template<std::floating_point, typename>
struct Test {};
// (2)
using call1 = MakeCallable<Test>;
// 컴파일 오류! 제약조건이 충족되지 않습니다.
call1::Invoke<int, int>;
// 문제 없음.
// call1::Invoke == Test
// call1::Invoke<float, int> == Test<float, int>
call1::Invoke<float, int>;
상기 코드는 Invoke
와 MakeCallable
을 사용하는 법을 보여주고 있다.8.3. Sort (정렬)
#!syntax cpp
template<typename Predicate, typename...>
struct MetaComparableImpl : std::false_type {};
template<typename Predicate, typename T, typename U>
requires (requires { typename ::Invoke<Predicate, T, U>; })
struct MetaComparableImpl<Predicate, T, U> : std::true_type
{};
template<typename Predicate, typename T, typename U, typename... Us>
requires (requires { typename ::Invoke<Predicate, T, U>; })
struct MetaComparableImpl<Predicate, T, U, Us...> : MetaComparableImpl<Predicate, U, Us...>
{};
template<typename Predicate, typename... Ts>
constexpr bool MetaComparable_v = MetaComparableImpl<Predicate, Ts...>::value;
template<typename Predicate, typename... Ts>
concept MetaComparable = MetaComparable_v<Predicate, Ts...>;
template<typename... Ts>
struct SameAll : std::false_type {};
template<typename T, typename... Ts>
struct SameAll<T, Ts...> : std::conjunction<std::is_same<T, Ts>...> {};
template<typename... Ts>
constexpr bool same_all_v = SameAll<Ts...>::value;
template<typename... Ts>
concept IsSameAll = same_all_v<Ts...>;
정렬을 구현하기 전에 필요한 유틸리티를 정의하자.#!syntax cpp
template<typename Comparator, typename TList>
struct Sort;
// 빈 리스트
template<typename Comparator>
struct Sort<Comparator, TypeList<>>
{
using type = TypeList<>;
template <typename L, typename R>
using Compare = Invoke_t<Comparator, L, R>;
};
// 원소가 전부 동일한 리스트
template<typename Comparator, typename... Ts> requires IsSameAll<Ts...>
struct Sort<Comparator, TypeList<Ts...>>
{
using type = TypeList<Ts...>;
template <typename L, typename R>
using Compare = Invoke_t<Comparator, L, R>;
};
// 2차 정렬
template<typename Comparator // 비교기
, typename Current // 현재 순회 중인 리스트
, typename Indexer> // 순열
struct SortImpl2;
// 2차 정렬 종료
template<typename Comparator, typename Current, size_t LastIndex>
struct SortImpl2<Comparator, Current, std::index_sequence<LastIndex>>
{
using type = Current;
};
template<typename Comparator // 비교기
, typename Current // 현재 순회 중인 리스트
, size_t J // 현재 원소 U의 순번
, size_t K // 다음 원소 T의 순번
, size_t... Js> // 나머지 순번
struct SortImpl2<Comparator, Current, std::index_sequence<J, K, Js...>>
: SortImpl2<Comparator
, std::conditional_t
<
Invoke_t
<
Comparator
, At_t<Current, J>
, At_t<Current, K>
>::value
, Current
, SwapElementAt_t<Current, J, K>
> // Result
, std::index_sequence<K, Js...>> // Indexer
{};
// 1차 정렬
template<typename Comparator, typename TList, typename Indexer>
struct SortImpl;
// 1차 정렬 종료
template<typename Comparator, typename TList>
struct SortImpl<Comparator, TList, std::index_sequence<>>
{
using type = TList;
};
// 1차 정렬 중간
template<typename Comparator, typename TList, size_t I, size_t... Is>
struct SortImpl<Comparator, TList, std::index_sequence<I, Is...>>
: SortImpl<Comparator, SortImpl2<Comparator, TList, std::make_index_sequence<GetLength_v<TList> - I>>::type, std::index_sequence<Is...>>
{};
// 버블 정렬
template<typename Comparator, typename... Ts> requires MetaComparable<Comparator, Ts...>
struct Sort<Comparator, TypeList<Ts...>>
: SortImpl<Comparator, TypeList<Ts...>, std::make_index_sequence<sizeof...(Ts) - 1>>
{
template <typename L, typename R>
using Compare = Invoke_t<Comparator, L, R>;
};
template<typename TList, typename Comparator>
using Sort_t = Sort<Comparator, TList>::type;
메타 함수를 이진 비교기로 써서 리스트를 정렬하는 메타 함수는 위와 같이 구현할 수 있다. 버블 정렬을 사용했다. 버블 정렬은 O(n²)
으로 느리지만, 어차피 컴파일러가 전부 다 처리해주므로 런타임엔 아무 문제도 없다. 상기 코드에서는 for
문을 std::index_sequence
로 치환하여 작성했다.- <C++ 예제 보기>
#!syntax cpp template<typename L, typename R> struct TestComparator : std::bool_constant<(sizeof(L) <= sizeof(R))> {}; using Comparator = MakeCallable<TestComparator>; // (1) TypeList<int, long> Sort_t<TypeList<int, long>, Comparator>; // (2) TypeList<char, bool, unsigned, long, int, int, double&, double> Sort_t<TypeList<double&, char, unsigned, double, long, int, int, bool>, Comparator>; struct Large { char bytes[100];} ; // (3) TypeList<unsigned char, short, float, int, double, unsigned long long, Large> Sort_t<TypeList<double, float, Large, unsigned long long, short, unsigned char, int>, Comparator>;
8.4. IsSorted (정렬)
#!syntax cpp
template<typename TList, typename Comparator>
struct IsSorted : std::is_same<TList, Sort_t<TList, Comparator> {};
template<typename TList, typename Comparator>
constexpr bool IsSorted_v = std::is_same_v<TList, Sort_t<TList, Comparator>>;
원래 리스트와 정렬된 리스트와를 비교해서 같은지 여부를 판별한다.8.5. Bind
혹시 다른 언어나<functional>
에서 함수의 인자를 실행전에 선제적으로 할당하는 기능이 있다는 것을 아는가? 예를 들어서 void Function(int, int, const char*, bool);
이란 함수가 있을 때, auto fun = Function(arg1 = 10, arg2 = "Hello");
와 같이 할당했다고 하자. 그럼 두번째와 세번째 인자는 전달하지 않아도 된다. 곧 Function(20, true);
처럼 호출할 수 있다. 이런 종류의 기능을 인자를 매개변수에 선제적으로 귀속(Bind)한다고 말한다. 메타 프로그래밍도 비슷한 행위가 가능하다. 자리 지시자(Placeholder)를 써서 위치를 자유롭게 귀속하는 건 너무 복잡해서 넘어가지만, 대신 템플릿 매개변수 묶음의 앞이나 뒤에 인자를 미리 전달하는 방법을 써보자.#!syntax cpp
template <typename Functor, typename... Args>
struct Bind
{
template <typename... Params>
using Invoke = ::Invoke<Functor, Params..., Args...>;
};
`Functor`
의 앞쪽 매개변수들을 Params...
로 대체한다.#!syntax cpp
using Caller = MakeCallable<std::is_same>;
// Binder::Invoke == MakeCallable<std::is_same, int, ...>
using Binder = Bind<Caller, int>;
// Result == std::is_same<int, int>
using Result = Invoke_t<Binder, int>;
예시 실행 결과는 위와 같다.8.6. Apply
앞서 템플릿 문서에서 함자에 튜플을 전달해서 실행하는 예제를 보고, 이 문서에서 확장까지 했다. 메타 프로그래밍에서도 마찬가지 행위가 가능하다.#!syntax cpp
template <typename Functor, typename TList>
struct Apply;
template <typename Functor, typename... Ts>
struct Apply<Functor, TypeList<Ts...>>
{
using Invoke = ::Invoke_t<Functor, Ts...>;
};
template <typename Functor, typename TList>
using Apply_t = Apply<Functor, TList>::Invoke::type;
`Apply`
는 메타 함수 `Functor`
에 `TypeList<Ts...>`
로 매개변수를 전달해서 실행한 결과를 반환한다.만약에
`Functor`
가 또 다른 type
자료형을 반환하는 메타 함수라면 유틸리티 `Apply_t`
를, 그렇지 않으면 `Apply`
만 써도 된다.#!syntax cpp
// (1)
using Caller0 = MakeCallable<std::add_const>;
// Apply_t == const int
Apply_t<Caller0, TypeList<int>> a{ 50 };
// (2)
using Caller1 = MakeCallable<Test>;
// 컴파일 오류!
Apply<Caller1, TypeList<int, int>>::Invoke;
// 문제없음. Apply<...>::Invoke == Test<float, int>
Apply<Caller1, TypeList<float, int>>::Invoke;
예시 실행 결과는 위와 같다.8.7. Transform
#!syntax cpp
template <typename Functor, typename TList>
struct Transform;
template <typename Functor, typename... Ts>
struct Transform<Functor, TypeList<Ts...>>
{
using type = TypeList<Invoke_t<Functor, Ts>...>;
};
template <typename TList, typename Functor>
using Transform_t = Transform<TList, Functor>::type;
자료형 리스트의 원소 전부에 메타 함수를 적용하는 메타 함수는 위와 같이 구현할 수 있다.#!syntax cpp
using Caller = MakeCallable<std::add_lvalue_reference>;
// Result == TypeList<short&, float&, int&, long long&, char&, bool&>
using Result = Transform_t<TypeList<short, float, int, long long, char, bool>, Caller>;
예시 실행 결과는 위와 같다.9. 예제 7: 자료형 안전한 union 구현
이 문단에서는 바퀴의 재발명을 하고자 하며, 또한 C언어의 유산을 극복하기가 얼마나 어려운지 알아보는 시간을 가지도록 하자. 구현할 내용은 Discriminted Union(구별되는 공용체) 내지는 Tagged Union(이름있는 공용체)라고 칭하는union
(공용체)의 대안 클래스다.9.1. 개론: 공용체의 성질
|
공용체는 데이터 멤버와 멤버 함수 및 자료형 별칭, 혹은 접근 지정자를 포함할 수 있는 등 구조체와 비슷한 객체다. 그런데 공용체는 내부의 비정적 데이터 멤버들이 단일한 메모리 위치로 모이는 성질을 가지고 있다. 즉 내부의 비정적 데이터 멤버들이 겹쳐질 수 있다. 대신 한번에 하나의 데이터 멤버만 활성화(Active)되고 사용할 수 있다.
최적화나 메모리 캐스팅을 줄이는 등 깔끔한 코딩이라는 장점이 있다. 가장 큰 바이트 크기의 데이터 멤버를 기준으로 공용체의 크기가 정해지므로 만약 다수의 정보를 한번에 조작할 필요가 없다거나, 상태가 없는 객체를 다룰 때 공용체를 사용할 수 있다. 그러나 단점도 있는데, 일단 C언어 기준으로는 수동 메모리 해제가 필수적인 점 빼고는 큰 문제가 아니었음을 알린다. 첫번째로 공용체의 멤버 중 어느 것이 활성화되었는지 알 방법이
C++26
전까지 없었다 [1]. union
은 포함된 정보가 어떻게 생겼는지만 알 수 있고, 할당한 값을 알 수 없고, 복사와 이동 생성자와 할당 연산자도 생성되지 않으며, 게다가 소멸자까지 클래스가 가져야 할 필수 멤버를 가질 수가 없었다. 두번째로 그 결과 소멸자를 직접 정의해줘야 하는데, 정의해주지 않으면 소멸자가 호출되지 않아 메모리 해제가 안 일어나는 치명적인 문제가 있었다. C언어 방식대로 malloc()
, free()
을 쓰면 큰 문제는 없지만, C++14
까지 상수 표현식에도 못 쓰고 구시대에서 내려온 기술로 잔존한 상태였다. new
, delete
마냥 조심해서 써야 하는 존재가 되었다.그래서 이를 대체하기 위한 수많은 기술이 도입되었는데 특히 동적 할당과 숫자 인덱스(태그)를 동원해 구현하는 방식이 주류였다. 그러다가
C++11
에서 가변 템플릿이 도입되면서 동적 할당을 내치고 상수 표현식으로의 전환을 포함하여 고성능 대체제가 도입되기 시작했다. C++17
에서 표준 라이브러리에 도입된 std::variant<Ts...>
는 성공적인 대체제 중 하나이며 당연히 상수 표현식을 지원한다.이를 구현하기 전에 먼저 공용체의 명세를 살펴보자.
#!syntax cpp
// 선언
union EmptyUnion;
// 정의
union EmptyUnion {};
빈 공용체는 위와 같이 정의한다. 구조체나 클래스 처럼 선언과 정의를 분리할 수 있다.#!syntax cpp
EmptyUnion uvar0;
EmptyUnion uvar1{};
EmptyUnion uvar2 = {};
const EmptyUnion uvar3;
const EmptyUnion uvar4{};
constexpr EmptyUnion uvar5{};
빈 공용체는 위와 같이 필드를 정의할 수 있다. 상수 표현식으로도 정의할 수 있다.이제 공용체에 멤버를 추가해보자. 클래스 처럼 정적 멤버, 자료형 별칭, 생성자, 대입 연산자, 멤버 함수와 비정적 데이터 멤버를 모두 선언하겠다.
- <C++ 예제 보기>
#!syntax cpp union [[nodiscard]] Union { static constexpr size_t size = std::max(sizeof(int), sizeof(float)); using type = std::string; // constexpr 아님. Union() = default; constexpr Union &operator=(float v) noexcept { v1 = v; return *this; } // 상수 표현식이 아니면 v0이 활성화 됐는지 아닌지는 상관없음. void Print() const { std::println("{}", v0); } int v0; float v1; };
9.1.1. 자명한 공용체
#!syntax cpp
union TrivialUnion
{
int v0;
float v1;
std::monostate v2;
};
멤버를 가진 공용체는 위와 같이 정의할 수 있다. 상기 코드의 공용체는 자명한 자료형 데이터 멤버만을 가지고 있다. std::monostate
는 아무 멤버도 없는 4바이트 짜리 열거형이다.#!syntax cpp
TrivialUnion tuvar0;
TrivialUnion uvar1{};
TrivialUnion tuvar2 = {};
const TrivialUnion tuvar3;
const TrivialUnion tuvar4{};
constexpr TrivialUnion tuvar5{};
마찬가지로 자명한 공용체는 상수 표현식에도 사용할 수 있다.#!syntax cpp
// (1) int
TrivialUnion tuvar6 = { .v0 = 0 };
// (2) float
constexpr TrivialUnion tuvar7{ .v1 = 1.5f };
// (3) std::monostate
constexpr TrivialUnion tuvar8{ .v2 = {} };
중요한 사실은 공용체에도 지정 초기화 (Designated Initialization)를 쓸 수 있다는 점이다. 또한 C언어와는 달리 C++의 지정 초기화는 멤버 순서를 준수해야 하지만 공용체는 상관없다. 어차피 멤버 하나만 사용하므로. 여전히 상수 표현식에도 이용할 수 있다.#!syntax cpp
// (1) tuvar9.v0 = 5
constexpr TrivialUnion tuvar9{ 5.7 };
// (2) tuvar10.v0 = 2
constexpr float inv = 2.5f;
constexpr TrivialUnion tuvar10{ inv };
// (3) 컴파일 오류!
constexpr TrivialUnion tuvar11{ std::monostate{} };
이제 까다로운 지점이 온다. 초기화를 호출하면 첫번째 멤버만 초기화된다. 심지어 첫번째 멤버에 생성자가 없어도 다음 멤버를 호출하지 않는다.#!syntax cpp
struct HiddenCtor
{
private:
HiddenCtor() = default;
};
union HiddenUnion
{
HiddenCtor v0;
int v1;
float v2;
};
// (1) 컴파일 오류! 삭제된 생성자 `HiddenUnion::HiddenUnion()`를 참조하려고 합니다.
HiddenUnion huvar0;
// (2) 컴파일 오류! 생성자 `HiddenUnion::HiddenUnion()`에 접근할 수 없습니다.
const HiddenUnion huvar1{};
// (3) 컴파일 오류! 생성자 `HiddenUnion::HiddenUnion()`에 접근할 수 없습니다.
constexpr HiddenUnion huvar2{};
// (4) 컴파일 오류! 적절한 생성자가 없습니다.
HiddenUnion huvar3{ 0 };
상기 코드는 생성자가 숨겨졌을 때 (또는 삭제되었을 때) 공용체에 무슨 일이 일어나는지를 보여주고 있다. 공용체에 대한 생성자 호출은 반드시 첫번째 멤버만을 호출한다. 또한 이 때문에 생성자가 암시적으로 생성되지 않는다.#!syntax cpp
union TrivialUnion
{
// 정의 안해도 됨.
TrivialUnion(const TrivialUnion &) noexcept = default;
TrivialUnion(TrivialUnion &&) noexcept = default;
// 정의 안해도 됨.
TrivialUnion &operator=(const TrivialUnion &) noexcept = default;
TrivialUnion &operator=(TrivialUnion &&) noexcept = default;
// 정의 안해도 됨.
~TrivialUnion() noexcept = default;
};
마지막으로, 기본 멤버 함수를 살펴보고 넘어가자. 자명한 공용체는 기본 생성자를 빼고 특수 멤버 함수가 암시적으로 생성된다. 전부 default
에 noexcept
이므로 굳이 명시를 안해도 문제가 없다. 또한 소멸자도 암시적으로 생성되므로 멤버의 소멸자가 호출되지 않을 걱정은 없다. 굳이 생성자를 정의하지 말고 지정 초기화를 써도 문제없다.#!syntax cpp
TrivialUnion uvar0{ .v0 = 3 };
TrivialUnion uvar1{ .v1 = 9.9f };
// 자명한 공용체의 연산은 상수 표현식에서도 쓸 수 있음.
// 이전에 대입된 `float v1`의 메모리는 명시하지 않아도 알아서 해제됨.
// uvar1 == TrivialUnion{ .v0 = 3 }
uvar1 = uvar0;
만약 복사나 이동으로 인해 이미 활성화된 멤버 말고 다른 멤버가 대입되었을 경우, 자명한 자료형은 소멸자가 알아서 호출되고 메모리가 해제된다. 그 다음에 새로운 멤버가 전달되어 활성화된다.9.1.2. 비자명한 공용체
#!syntax cpp
struct NonTrivialStruct
{
// 사용자 정의 생성자
// 상수 표현식이지만 데이터 멤버 `ref` 때문에 자명하지 않은 자료형임에 유의
constexpr NonTrivialStruct(int& outref) noexcept : ref(outref) {}
int& ref;
};
union NonTrivialUnion
{
int v0;
double v1;
NonTrivialStruct v2;
};
이제 비자명한 공용체를 보자. 가장 쉬운 예는 참조자를 비정적 멤버로 가지는 공용체다. 주의할 점은 공용체에는 직접 참조자를 쓸 수 없다. 원본 변수는 분명 공용체 바깥에 위치할 텐데 공용체 내부에 있는 필드와 겹쳐진다는 것이 논리적으로 안 맞기 때문이다. 대신 비자명한 구조체 혹은 클래스를 멤버로 가진 공용체도 비자명하니까 이를 통해 전달할 수는 있다. 원래 공용체는 생성자가 자동으로 생성되지 않는다. 이제 비자명한 공용체는 소멸자와 복사/이동 대입 연산자 조차 자동으로 생성되지 않는다. [2] 우리가 직접 구현해야할까? 상황에 따라 다르다.#!syntax cpp
struct NonTrivialStruct
{
constexpr NonTrivialStruct(int& outref) noexcept : ref(outref) {}
int& ref;
};
static_assert(std::is_trivial_v<NonTrivialStruct>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_constructible_v<NonTrivialStruct>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_copy_constructible_v<NonTrivialStruct>);
static_assert(std::is_trivially_move_constructible_v<NonTrivialStruct>);
static_assert(std::is_trivially_copy_assignable_v<NonTrivialStruct>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_move_assignable_v<NonTrivialStruct>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_destructible_v<NonTrivialStruct>);
// 일단 `int&`는 비자명하지만, 원시 자료형이라서 복사/이동 생성자와 소멸자는 내부적으로 암시적으로 생성됨.
// 소멸자가 자동으로 생성되어 `NonTrivialUnion`에 소멸자를 정의할 필요가 없음.
// `int&` 때문에 복사/이동 대입 연산자는 삭제됨.
union NonTrivialUnion
{
int v0;
double v1;
NonTrivialStruct v2;
};
static_assert(std::is_trivial_v<NonTrivialUnion>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_constructible_v<NonTrivialUnion>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_copy_constructible_v<NonTrivialUnion>);
static_assert(std::is_trivially_move_constructible_v<NonTrivialUnion>);
static_assert(std::is_trivially_copy_assignable_v<NonTrivialUnion>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_move_assignable_v<NonTrivialUnion>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_destructible_v<NonTrivialUnion>);
상기 코드는 비자명한 자료형에 대해 자명성을 검사하는 메타 함수를 실행한 결과를 보여주고 있다.중요한 것은
`NonTrivialUnion`
은 여전히 자명하게 해제할 수 있다는 사실이다. [3] 그래서 소멸자를 만들지 않아도 된다.소멸자를 명시해야하는 조건은 첫번째로
std::is_trivially_destructible<T>
가 false
인 자료형을 담은 경우가 있다. 두번째로 사용자 정의 기본 생성자 또는 사용자 정의 소멸자를 가진 자료형을 담은 경우가 있다. 그럼 이제 명시해야하는 예를 보자.#!syntax cpp
union NonTrivialUnionStr
{
// 소멸자 정의가 필요함. 내용은 작성안해도 됨.
// constexpr ~NonTrivialUnionStr() {}
// 명시하지 않으면 생성 시도도 안함.
NonTrivialUnionStr(const NonTrivialUnionStr &) = default;
NonTrivialUnionStr(NonTrivialUnionStr &&) = default;
NonTrivialUnionStr &operator=(const NonTrivialUnionStr &) = default;
NonTrivialUnionStr &operator=(NonTrivialUnionStr &&) = default;
int v0;
double v1;
std::string v2;
};
static_assert(std::is_trivial_v<NonTrivialUnionStr>); // 컴파일 오류! 정적 어설션이 실패했습니다.
static_assert(std::is_trivially_constructible_v<NonTrivialUnionStr>); // 상동
static_assert(std::is_trivially_copy_constructible_v<NonTrivialUnionStr>); // 상동
static_assert(std::is_trivially_move_constructible_v<NonTrivialUnionStr>); // 상동
static_assert(std::is_trivially_copy_assignable_v<NonTrivialUnionStr>); // 상동
static_assert(std::is_trivially_move_assignable_v<NonTrivialUnionStr>); // 상동
static_assert(std::is_trivially_destructible_v<NonTrivialUnionStr>); // 상동
상기 코드는 표준 라이브러리의 문자열 클래스 std::string
을 멤버로 가진 공용체와 표명문 검사를 보여주고 있다. std::string
은 사용자 정의 기본 생성자와 소멸자를 가지고 있어서 자명하지 않은 자료형이다. 참고로 C++20
부터 상수 시간에 힙 메모리 할당이 가능해지면서 std::string
의 멤버 함수들이 constexpr
를 얻었다. 그래서 상기 예제의 `NonTrivialUnionStr`
도 자명하지 않은 공용체가 된다. 자명하지 않으므로 복사/이동 생성자와 대입 연산자도 생성하라고 지시해야한다. 안 그러면 생성되지 않는다.이제 자명한 공용체와 비자명한 공용체 사이에 처리를 달리해야 함을 알 수 있다. 우리가 만들 안전한 공용체는 템플릿으로 다수의 자료형을 받아서 내부적으로 알아서 돌아가게 만들 것인데, 자명성에 따라 클래스를 특수화해야 한다는 걸 알 수 있다. 그런데
C++20
이후로 상황이 달라졌다. 제약조건의 추가로 멤버 함수의 존재를 requires
구문으로 명시할 수 있게 되었다. 굳이 템플릿 특수화 등으로 처리를 나눌 필요 없이 하나의 클래스에 제약조건을 여기저기 붙이면 한눈에 코드를 알아볼 수 있다.9.1.3. 예제 8: 이름붙인 공용체 (Tagged Union)
본문으로 가기 전에, 수동으로 공용체를 관리하는 방법을 알아보자. 바로 꼬리표(Tag) 열거형을 이용해서 관리하는 공용체다. 이름붙인 공용체는 문자 그대로 저장한 멤버들 마다 열거형으로 이름으로 붙여주고 내부의 공용체에 저장한다. 값의 설정이나 반환 시에는 열거형을 전달하는 식으로 동작한다.- <C++ 예제 보기>
#!syntax cpp enum class Tags { Nothing = 0, // 원시 자료형 Bool, Int, String }; struct WrongAccessUnion { explicit WrongAccessUnion() noexcept = default; }; inline constexpr WrongAccessUnion wrong_union_access_error = {}; class TaggedUnion { public: constexpr TaggedUnion() noexcept : myTag(Tags::Nothing), emptyValue() {} constexpr TaggedUnion(const bool &flag) noexcept : myTag(Tags::Bool), v0(flag) {} constexpr TaggedUnion(const int &value) noexcept : myTag(Tags::Int), v1(value) {} template<size_t L> constexpr TaggedUnion(const char(&value)[L]) : myTag(Tags::String), v2(value) {} constexpr TaggedUnion(const std::string &value) : myTag(Tags::String), v2(value) {} constexpr TaggedUnion(std::string &&value) : myTag(Tags::String), v2(std::move(value)) {} constexpr ~TaggedUnion() { switch (myTag) { case Tags::Nothing: case Tags::Bool: case Tags::Int: {} break; case Tags::String: { // 소멸자 직접 호출 v2.~basic_string(); } break; default: break; } } [[nodiscard]] constexpr Tags GetTag() const noexcept { return myTag; } // noexcept가 아님. 컴파일 시점에선 어떤 값이 활성화 되었는지 알 수 있으므로 값을 얻지 못하면 컴파일이 안됨. template<Tags Tag> [[nodiscard]] constexpr auto GetValue() const { if constexpr (Tag == Tags::Bool) { return v0; } else if constexpr (Tag == Tags::Int) { return v1; } else if constexpr (Tag == Tags::String) { return v2; } else { return wrong_union_access_error; } } template<Tags Tag> [[nodiscard]] constexpr auto TryGetValue() const noexcept { if constexpr (Tag == Tags::Bool) { if (myTag == Tag) { return std::expected<bool, WrongAccessUnion>{ v0 }; } else { return std::unexpected{ wrong_union_access_error }; } } else if constexpr (Tag == Tags::Int) { if (myTag == Tag) { return std::expected<int, WrongAccessUnion>{ v1 }; } else { return std::unexpected{ wrong_union_access_error }; } } else if constexpr (Tag == Tags::String) { if (myTag == Tag) { return std::expected<std::string, WrongAccessUnion>{ v2 }; } else { return std::unexpected{ wrong_union_access_error }; } } else { if (myTag == Tag) { return std::expected<std::monostate, WrongAccessUnion>{ emptyValue }; } else { return std::unexpected{ wrong_union_access_error }; } } } private: Tags myTag = Tags::Nothing; union { std::monostate emptyValue; bool v0; int v1; std::string v2; }; };
중요한 사실은 우리가 앞으로 구현할 모든 클래스도 원시 공용체를 사용할 예정이다. 왜냐하면 공용체를 쓰지 않으면 다시 위험한 영역(Unsafe)에 발을 들이며 상수 표현식을 쓰지 못하기 때문이다. 가령
void*
에 malloc
을 가장 큰 자료형 크기만큼 할당하고 reinterpret_cast
로 형변환 및 수동으로 생성자와 소멸자를 호출해줘야 하는데 그걸 구현한 코드만으로 문서를 새로 분할해야 할 것이다. 그것보다는 C++20
부터 눈부시게 발전한 상수 표현식의 도움을 받아 (여전히 복잡하지만) 속편한 코딩을 하는 게 낫다. 눈이 불편하지만 적어도 한번 구현하고 버리는 코드는 아니게 한다는 말이다.- <C++ 예제 보기>
#!syntax cpp enum class Tags { Nothing = 0, Bool, I8, U8, I16, U16, I32, U32, I64, U64, I128, U128, F16, F32, F64, F80, Void, VoidPtr, I8Ptr, U8Ptr, I16Ptr, U16Ptr, I32Ptr, U32Ptr, // ... 등등 원시 자료형에 대한 포인터 Custom, // 이후로 계속 추가... };
9.2. 서론: 구별되는 공용체 (Discriminated Union)
이름붙인 공용체를 살짝 확장해보자. 그런데 어떻게 할까? 우리가 원하는 자료형을 넣지 못하는 게 치명적인 문제라고 했었다. 그런데 그 전에, 공용체 자료의 구분을 위해 정의한 열거형`Tags`
의 역할이 무엇이었는지 확인할 필요가 있다. `Tags`
는 물론 자료형을 구별하는 기능을 하지만, 그건 사용자의 입장이고 제작자의 입장은 다르다. 더 중요한 것은 이미 저장할 데이터가 클래스의 멤버로 선언되어 있다는 거고 그걸 구별하기 위해 일단 직관적인 열거형을 쓴 것 뿐이다. 우리가 구현할 때는 자료형마다 번호를 붙이는 것만으로 충분하지 않을까? 우리가 원하는 자료형을 아무거나 넣고 싶은데 그러려면 일단 열거형의 그늘에서 벗어나야 하지 않을까?- <C++ 예제 보기>
#!syntax cpp class TaggedUnion { public: static constexpr size_t npos = static_cast<size_t>(-1); constexpr TaggedUnion() noexcept : dataIndex(npos), emptyValue() {} constexpr TaggedUnion(const bool& flag) noexcept : dataIndex(0), v0(flag) {} constexpr TaggedUnion(const int& value) noexcept : dataIndex(1), v1(value) {} template<size_t L> constexpr TaggedUnion(const char(&value)[L]) : dataIndex(2), v2(value) {} constexpr TaggedUnion(const std::string& value) : dataIndex(02), v2(value) {} constexpr TaggedUnion(std::strin && value) : dataIndex(02), v2(std::move(value)) {} constexpr ~TaggedUnion() { switch (dataIndex) { case 0: // bool case 1: // int {} break; case 2: // std::string { v2.~basic_string(); } break; default: break; // npos, etc. } } [[nodiscard]] constexpr size_t GetIndex() const noexcept { return dataIndex; } template<size_t Index> [[nodiscard]] constexpr auto GetValue() const { if constexpr (0 == Index) { return v0; } else if constexpr (1 == Index) { return v1; } else if constexpr (2 == Index) { return v2; } else { return wrong_union_access_error; } } template<size_t Index> [[nodiscard]] constexpr auto TryGetValue() const noexcept { if constexpr (0 == Index) { if (Index == dataIndex) { return std::expected<bool, WrongAccessUnion>{ v0 }; } else { return std::unexpected{ wrong_union_access_error }; } } else if constexpr (1 == Index) { if (Index == dataIndex) { return std::expected<int, WrongAccessUnion>{ v1 }; } else { return std::unexpected{ wrong_union_access_error }; } } else if constexpr (2 == Index) { if (Index == dataIndex) { return std::expected<std::string, WrongAccessUnion>{ v2 }; } else { return std::unexpected{ wrong_union_access_error }; } } else { if (Index == dataIndex) { return std::expected<std::monostate, WrongAccessUnion>{ emptyValue }; } else { return std::unexpected{ wrong_union_access_error }; } } } private: size_t dataIndex; union { std::monostate emptyValue; bool v0; int v1; std::string v2; }; };
#!syntax cpp
template<typename... Ts>
class DiscriminatedUnion
{
public:
size_t dataIndex;
DiscriminatedUnion();
~DiscriminatedUnion();
union
{
Ts... values;
};
};
그래서 코드를 쓰려는데, 공용체에 넣을 멤버를 뭘로 할지 모르겠다. 이전에 구현했던 튜플과 비슷한 상황이다. 그때 분명히 정적이 아닌 데이터 멤버는 템플릿이 될 수 없다고 했다. 지금 정확히 일러주자면, template<...>
이 붙은 데이터 멤버가 안된다는 뜻이고 실체화한 데이터 멤버는 가능하다. 무슨 말이냐 하면, 우리가 공용체를 쓸 때는 template<...>
안에 이미 여러 자료형이 전달된 상태일텐데, 그 말은 클래스에 정의된 또다른 공용체도 실체화된 상태라는 말이다. 다시 말하면 우리가 실제로 공용체 클래스를 사용할 때는 클래스 내부에 공용체를 DiscriminatedUnion<int, float, ...> values;
와 같이 들고 있을 테니까 문제가 없다는 말이다.#!syntax cpp
template<typename T, typename... Ts>
class DiscriminatedUnion
{
public:
bool hasValue = false;
union
{
T myValue;
DiscriminatedUnion<Ts...> myTail;
};
};
상기 코드 처럼 이미 실체화가 된 템플릿 클래스가 멤버로 들어가는 건 문제가 아니다. 공용체 클래스는 포함(Composite) 관계로 정의되며 상속 관계로 정의한 튜플과는 다르게 구현해야 한다.#!syntax cpp
template<>
class DiscriminatedUnion<>;
참고로 빈 공용체 클래스도 구현해야 한다. 제일 마지막에 정의된 클래스가 빈 공용체 클래스를 멤버로 가지고 있을 것이므로.- <C++ 예제 보기>
#!syntax cpp template<typename T> concept Trivial = std::is_trivial_v<T>; template<typename T> concept TriviallyDetructible = std::is_trivially_destructible_v<T>; template<typename... Ts> concept TriviallyDetructibles = (std::is_trivially_destructible_v<Ts> && ...);
#!syntax cpp
// 자명하게 해제할 수 있는 공용체 클래스
template<TriviallyDetructible T, TriviallyDetructible... Ts>
class DiscriminatedUnion<T, Ts...>
{
// constexpr 기본 생성자, constexpr 소멸자, constexpr 복사/이동 생성자
// 복사/이동 대입 연산자는 있을 수도, 없을 수도.
};
// 자명한 공용체 클래스
template<Trivial T, Trivial... Ts>
class DiscriminatedUnion<T, Ts...>
{
// constexpr 기본 생성자, constexpr 소멸자, constexpr 복사/이동 생성자, constexpr 복사/이동 대입 연산자
};
제일 먼저 고려할 수 있는 건 특수화를 해주는 것이다. 만약 자명한 공용체라면 우리가 할게 거의 없고 인덱스 관리만 제때 해주면 된다. 비자명한 공용체는 해줄게 많다. 그러나 고작 복사/이동 대입 연산자 때문에 특수화를 나누는 건 일거리만 늘리는 거다. = default;
를 추가한다고 성능에 영향이 있는 것도 아니므로 제약조건으로 멤버 함수를 제한하고, 복사/이동 대입 연산자만 = default;
로 선언해주면 된다. 우리 목적은 데이터 저장하는 거지 복사 이동 못하는 자료형을 아득바득 memcpy
따위로 조작하는 그런 행위가 아니다.- <C++ 예제 보기>
#!syntax cpp template<typename... Ts> class DiscriminatedUnion { public: // Ts...의 자료형은 전부 달라야 함. explicit constexpr DiscriminatedUnion() noexcept(...) = default; constexpr ~DiscriminatedUnion() noexcept(...) requires(Ts가 전부 자명한 자료형인 경우) = default; constexpr ~DiscriminatedUnion() noexcept(...) requires(Ts 중에 하나라도 자명한 자료형이 아닌 경우) { // 명시적 소멸자 호출 필수 // 원시 자료형의 경우 유사 소멸자 호출 덕분에 실행 가능함. } template<typename U, typename V> constexpr decltype(auto) SetValue(V&& value) noexcept(...); template<size_t Index, typename V> constexpr decltype(auto) SetValue(V&& value) noexcept(...); template<size_t Index,, typename... Args> constexpr decltype(auto) Emplace(Args&&... args) noexcept(...); template<typename U, typename... Args> constexpr decltype(auto) Emplace(Args&&... args) noexcept(...); template<typename U> [[nodiscard]] constexpr ReturnType GetValue() & noexcept(...); template<typename U> [[nodiscard]] constexpr ReturnType GetValue() const& noexcept(...); template<typename U> [[nodiscard]] constexpr ReturnType GetValue() && noexcept(...); template<typename U> [[nodiscard]] constexpr ReturnType GetValue() const&& noexcept(...); template<size_t Index> [[nodiscard]] constexpr ReturnType GetValue() & noexcept(...); template<size_t Index> [[nodiscard]] constexpr ReturnType GetValue() const& noexcept(...); template<size_t Index> [[nodiscard]] constexpr ReturnType GetValue() && noexcept(...); template<size_t Index> [[nodiscard]] constexpr ReturnType GetValue() const&& noexcept(...); template<typename U> constexpr bool TryDestroy() noexcept(...); template<size_t Index> constexpr bool TryDestroy() noexcept(...); constexpr DiscriminatedUnion& operator=(DiscriminatedUnion&) noexcept(...); constexpr DiscriminatedUnion& operator=(const DiscriminatedUnion&) noexcept(...); constexpr DiscriminatedUnion& operator=(DiscriminatedUnion&&) noexcept(...); constexpr DiscriminatedUnion& operator=(const DiscriminatedUnion&&) noexcept(...); [[nodiscard]] constexpr bool operator==(const DiscriminatedUnion&) noexcept(...); [[nodiscard]] constexpr size_t GetIndex() const noexcept; [[nodiscard]] constexpr bool HasValue() const noexcept; template<typename U> [[nodiscard]] constexpr bool CanHolds() const noexcept; };
union
을 소유하고 있다는 것이다. 우리는 union
의 메모리 공간 절약과 고성능의 장점을 취하고, 메타 프로그래밍, 일반화 프로그래밍 등을 모두 동원해 단점을 최소화시켜야 한다.- <C++ 예제 보기>
#!syntax cpp template<typename T, typename... Args> concept NothrowConstructible = (std::is_nothrow_constructible_v<T, Args...>); template<typename T, typename... Ts> concept NothrowAssignables = (std::is_assignable_v<T, Ts> && ...); template<typename T> concept NothrowDestructible = (std::is_nothrow_destructible_v<T>); template<typename... Ts> concept NothrowDestructibles = (std::is_nothrow_destructible_v<Ts> && ...); template<typename... Ts> concept NothrowCopyables = (std::is_nothrow_copy_constructible_v<Ts> && ...); template<typename... Ts> concept NothrowCopyAssignables = (std::is_nothrow_copy_assignable_v<Ts> && ...); template<typename... Ts> concept NothrowMovables = (std::is_nothrow_move_constructible_v<Ts> && ...); template<typename... Ts> concept NothrowMoveAssignables = (std::is_nothrow_move_assignable_v<Ts> && ...);
#!syntax cpp
template<typename T, typename... Ts>
class DiscriminatedUnion
{
public:
// 이걸 해도 하위 클래스에서 상위 클래스의 속성을 알 수가 없음.
friend class DiscriminatedUnion<Ts...>;
private:
// 중복되는 데이터 멤버
bool hasValue = false;
size_t dataIndex = -1;
union
{
T myValue;
DiscriminatedUnion<Ts...> myTail; // 이 클래스도 `hasValue`와 `dataIndex`를 가진다.
};
};
그럼 이제 구현해볼까? 라기에는 아직 한번더 확인할 것이 남았다. 우리가 상기 코드와 같이 데이터 멤버를 정의하면 데이터의 번호와 값 소유 여부가 모든 공용체 클래스에 존재하게 된다. 그러면 공용체 클래스의 크기가 자료형 하나 추가할 때마다 기하급수적으로 커져버린다. size_t는 보통 64비트 정수이므로 8
의 크기, bool
은 보통 1
의 크기를 가지지만 메모리 정렬 때문에 4
이상의 바이트 크기를 가질 수도 있다. 그럼 최소 12
바이트 씩 추가로 늘어난다. 아주아주 사소해보이지만 이런 공용체가 쌓이면 결국 성능에 영향이 갈 수 밖에 없다. 또한, 데이터 인덱스는 값을 할당할 때 참조할텐데 이 번호는 결국 최상위 클래스에서만 관리하는 게 더 효율적일 것이다. 하위 클래스에서 가지고 있어도 하위 클래스는 상위 클래스가 얼마나 많이 존재하는지 알 수가 없으므로 상대적 인덱스의 역할도 못한다. 함수에서 상위 클래스의 인스턴스를 참조형으로 전달하면 되지 않는가? 싶지만 그건 함수에서나 사용할 수 있고 메타 함수나 정적인 연산에선 쓰지 못한다. friend
를 선언해도 무의미하다.#!syntax cpp
template<typename T, typename... Ts>
struct DiscriminatedUnionBase
{
union
{
T myValue;
DiscriminatedUnionBase<Ts...> myTail;
};
};
template<>
struct DiscriminatedUnionBase<>
{};
template<typename... Ts>
class DiscriminatedUnion : private DiscriminatedUnionBase<Ts...>;
그러므로 공용체만 저장하는 전용 클래스를 만들고, 여기에 공용체를 위한 생성자, 대입 연산자, 값 할당 함수등을 전부 몰아 넣어야 한다. 이걸 구현하려면 매우 지저분하므로 private
상속을 하고 필요한 멤버만 노출하는 것이 좋다.9.3. 본론
- <C++ 예제 보기>
#!syntax cpp template<typename IndexType, typename T, typename TList> struct FindIndex; template<typename IndexType, typename T> struct FindIndex<IndexType, T, TypeList<>> : std::integral_constant<size_t, -1> { using result = MetaNotFoundError; }; template<typename IndexType, typename T, typename U, typename Indexer, typename TList> struct FindIndexImpl; template<typename IndexType> struct FindIndexImpl2 : std::integral_constant<IndexType, -1> { using result = MetaNotFoundError; }; template<typename IndexType, typename T, IndexType Index, typename... Us> struct FindIndexImpl<IndexType, T, T, std::integral_constant<IndexType, Index>, TypeList<Us...>> : std::integral_constant<IndexType, Index> { using result = T; }; template<typename IndexType, typename T, typename U, IndexType Index, typename V, typename... Vs> requires (!std::is_same_v<T, U>) struct FindIndexImpl<IndexType, T, U, std::integral_constant<IndexType, Index>, TypeList<V, Vs...>> : FindIndexImpl<IndexType, T, V, std::integral_constant<IndexType, Index + 1>, TypeList<Vs...>> {}; template<typename IndexType, typename T, typename U, typename... Us> struct FindIndex<IndexType, T, TypeList<U, Us...>> : std::conditional_t<Has_v<TypeList<U, Us...>, T> , FindIndexImpl<IndexType, T, U, std::integral_constant<IndexType, 0>, TypeList<Us...>> , FindIndexImpl2<IndexType>> {}; template<typename TList, typename U, typename IndexType = size_t> constexpr IndexType FindIndex_v = FindIndex<IndexType, U, TList>::value;
9.3.1. 공용체 저장소 클래스
- <C++ 예제 보기>
#!syntax cpp template<typename... Ts> struct DUStorage; template<> struct DUStorage<> { using value_type = void; using index_type = int; using size_type = std::make_unsigned_t<index_type>; }; template<typename T, typename... Ts> struct DUStorage<T, Ts...> { using value_type = T; using index_type = int; using size_type = std::make_unsigned_t<index_type>; constexpr DUStorage() noexcept {} constexpr ~DUStorage() noexcept requires(TriviallyDetructibles<T, Ts...>) = default; constexpr ~DUStorage() noexcept requires(!TriviallyDetructibles<T, Ts...>) {} constexpr DUStorage(const DUStorage &) noexcept(NothrowCopyables<T, Ts...>) = default; constexpr DUStorage &operator=(const DUStorage &) noexcept(NothrowCopyAssignables<T, Ts...>) = default; constexpr DUStorage(DUStorage &&) noexcept(NothrowMovables<T, Ts...>) = default; constexpr DUStorage &operator=(DUStorage &&) noexcept(NothrowMoveAssignables<T, Ts...>) = default; template<typename TopUnion, typename... Args> constexpr DUStorage(TopUnion &top, const index_type top_index, const std::in_place_type_t<T> &, Args&&... args) : value(std::forward<Args>(args)...) { top._dataPlace = top_index; } template<typename TopUnion, typename U, typename... Args> constexpr DUStorage(TopUnion &top, const index_type top_index, std::in_place_type_t<U>, Args&&... args) : tail(top, top_index + 1, std::in_place_type<U>, std::forward<Args>(args)...) {} union { T value; DUStorage<Ts...> tail; }; };
9.3.2. 공용체 프록시 클래스
- <C++ 예제 보기>
#!syntax cpp inline constexpr int discriminatedUnionOutbound = -1; template<typename... Ts> class DUBase; template<typename T, typename... Ts> class DUBase<T, Ts...> { public: using mirror_t = TypeList<T, Ts...>; using type = DUBase<T, Ts...>; using store_type = DUStorage<T, Ts...>; using tail_type = DUBase<Ts...>; using value_type = T; using index_type = store_type::index_type; using size_type = store_type::size_type; static constexpr std::size_t Size = 1 + sizeof...(Ts); // 공백 초기화 constexpr DUBase() : _empty() {} constexpr ~DUBase() noexcept requires(TriviallyDetructibles<T, Ts...>) = default; constexpr ~DUBase() noexcept requires(!TriviallyDetructibles<T, Ts...>) {} // 값 초기화 template<typename U, typename... Args> constexpr DUBase(std::in_place_type_t<U>, Args&&... args) : _hasValue(true), _data(*this, 0, std::in_place_type<U>, std::forward<Args>(args)...) {} /* private */ template<typename U, typename V> constexpr void _SetValue(std::in_place_type_t<U>, V&& value); /* private */ template<typename U, typename... Args> constexpr U& _Emplace(std::in_place_type_t<U>, Args&&... args); /* private */ template<typename U> constexpr U& _GetValue(std::in_place_type_t<U>); /* private */ template<typename U> constexpr const U& _GetValue(std::in_place_type_t<U>) const; /* private */ [[nodiscard]] constexpr index_type _GetIndex() const noexcept { return _dataPlace; } /* private */ [[nodiscard]] constexpr bool _HasValue() const noexcept { return _hasValue; } /* private */ template<typename U> [[nodiscard]] constexpr bool _Contains(std::in_place_type_t<U>) const noexcept { if constexpr (Has_v<mirror_t, U>) { return _dataPlace != discriminatedUnionOutbound && _dataPlace == FindIndex_v<mirror_t, U>; } else { return false; } } /* private */ template<index_type Index> [[nodiscard]] constexpr bool _Contains(const std::in_place_index_t<Index>&) const noexcept { if constexpr (Index < Size) { return _hasValue and _dataPlace == Index and discriminatedUnionOutbound != Index; } else { return false; } } /* public */ // 정의되지 않음. constexpr DUBase& operator=(const DUBase&) noexcept(NothrowCopyables<Ts...> and NothrowCopyAssignables<Ts...>); /* public */ // 정의되지 않음. constexpr DUBase& operator=(DUBase&&) noexcept(NothrowMovables<Ts...> and NothrowMoveAssignables<Ts...>); /* public */ // 정의되지 않음. [[nodiscard]] constexpr bool operator==(const DUBase&) const noexcept; /* public */ template<typename U> [[nodiscard]] constexpr bool CanHold() const noexcept { return Has_v<mirror_t, U>; } /* private */ bool _hasValue = false; /* private */ index_type _dataPlace = discriminatedUnionOutbound; /* private */ union { std::monostate _empty; store_type _data; }; };
std::monostate
를 하위 클래스가 가지고 있으면 더미 바이트가 하위 클래스에 쌓인다.#!syntax cpp
template<typename U, typename V>
constexpr void _SetValue(std::in_place_type_t<U>, V&& value);
/* private */
template<typename U, typename... Args>
constexpr U& _Emplace(std::in_place_type_t<U>, Args&&... args);
template<typename U>
constexpr U& _GetValue(std::in_place_type_t<U>);
template<typename U>
constexpr const U& _GetValue(std::in_place_type_t<U>) const;
그러나 상기 코드는 가장 중요한 세 함수를 정의하지 않았다. 값을 할당하거나 생성하는 `_SetValue`
, 값을 할당하는 `_Emplace`
, 값을 얻어오는 `_GetValue`
가 없다. 이 셋은 모두 특정 자료형을 std::in_place_type_t<T>
를 통해 전달한다.#!syntax cpp
template<typename U>
constexpr U& _GetValue(std::in_place_type_t<U>);
template<typename U>
constexpr const U& _GetValue(std::in_place_type_t<U>) const;
먼저 `_GetValue`
를 구현해보자. `U`
는 찾아낼 자료형이다.#!syntax cpp
template<typename U, typename Current>
constexpr U& _GetValueImpl(std::in_place_type_t<U>, Current& node)
{
if constexpr (std::is_same_v<typename Current::value_type, U>)
{
return *std::addressof(node.value);
}
else
{
// 재귀 호출
// 공용체를 저장하는 `store_type _data`는 하위 노드 `tail`을 가지고 있음.
return _GetValueImpl(std::in_place_type<U>, node.tail);
}
}
template<typename U>
constexpr U& _GetValue(std::in_place_type_t<U>)
{
return _GetValueImpl(std::in_place_type<U>, _data);
}
template<typename U>
constexpr const U& _GetValue(std::in_place_type_t<U>) const
{
return _GetValueImpl(std::in_place_type<U>, _data);
}
앞서 작성한 `DUStorage<...>`
는 내부에 tail
이라는 데이터 멤버를 통해 여러 공용체가 연결된 형태를 띄고 있다. 즉 하위 클래스의 tail
을 여러번 접근하면서 공용체에 저장된 다음 순서의 값을 읽을 수 있다. 이는 재귀 함수로 쉽게 구현할 수 있다.#!syntax cpp
template<typename U, typename V>
constexpr void _SetValueImpl(std::in_place_type_t<U>, V&& value);
다음은 `_SetValue`
를 구현해보자. `U`
는 할당할 자료형, `V`
는 할당할 값이다. 그리고 할당하려는 값을 제외한 다른 값을 모두 순회하면서 해제하는 방법도 알아보겠다.제일 먼저 언급할 점은 우리가 직접 값을 해제해야 하는 상황이 있음을 알아야 한다. 자명하지 않은 자료형은 새로운 값을 할당할 때 소멸자를 호출하지 않으므로 수동으로 해줘야 한다.
#!syntax cpp
template<typename U, typename V>
constexpr void _SetValue(std::in_place_type_t<U>, V&& value)
{
auto& ref = _GetValue(std::in_place_type<U>);
// 기존에 `U`와 다른 자료형의 값을 갖고 있으면 해제해줘야만 함.
// 만약 다른 자료형이 자명하면 소멸자를 호출하나 안하나 문제없음. 자명하지 않으면 반드시 호출해야 함.
ref = std::forward<V>(value);
}
가장 쉬운 사례는 이미 작성한 _GetValue
로 참조를 가져와서 값을 넣는 것이다. 값을 전달하기 전에 기존의 값을 해제해야 하는데, 해제할 값을 어떻게 찾을까? 우리가 공용체 내부의 값을 들여다 볼 방법이 있을까?#!syntax cpp
// 데이터 멤버
index_type __dataPlace = discriminatedUnionOutbound; // -1
// 멤버 함수 _SetValueAt
template<size_t Index>
void _SetValueAt(V&& value);
// 컴파일 오류!
_SetValueAt<__dataPlace>(...);
첫번째로 알아야 할 상황은 우리가 저장한 값의 번호는 동적으로 변화하는 값이라는 것이다. 컴파일 시점 상수가 아니므로 템플릿에 사용할 수 없다. 그러므로 공용체들이 각자 고정된 인덱스를 가지게 하고, `__dataPlace`
와 순서대로 비교하는 별도의 상수 표현식이 아닌 코드를 작성해야 한다. 가령 `DUStorage`
에 또다른 인덱스 변수 또는 템플릿으로 컴파일 상수를 정의해줄 수 있다. 하지만 저장소 클래스에 번호를 붙이려면 저장소 클래스의 코드를 고쳐야 한다. 그것보다는 템플릿 문서의 예제에서 구현한 대로 std::integer_sequence
와 내부 구현 함수를 써서 컴파일 시점에 반복되도록 구현해야 한다. 그래서 안타깝게도 무작위 탐색이 불가능하고 선형 탐색으로 찾아내야 한다.가장 먼저 고려할 수 있는 방안은 이미 작성한
_GetValue
마냥 재귀 호출로 공용체 노드를 열거하면서 찾는 방법이 있다. 그러나 재귀 구현은 매우 쉬우나 성능이 좋지 않다는 단점이 있다. _GetValue
의 구현도 이후 문단에서 최적화 할 예정이니까 다른 방법을 알아보자. 대신 사용할 방법은 방문자 패턴(Visitor Pattern)을 이용한 멤버 열거를 구현하고자 한다.#!syntax cpp
template<typename U, typename V>
constexpr void _SetValue(std::in_place_type_t<U>, V &&value)
{
_SetValueImpl(std::make_integer_sequence<index_type, Size>{}, std::in_place_type<U>, std::forward<V>(value));
}
// 멤버 함수 _SetValueImpl
template<typename U, typename V, index_type... Indices>
constexpr void _SetValueImpl(std::integer_sequence<index_type, Indices...>, std::in_place_type_t<U>, V&& value);
{
// (1) Indices...를 열거하면서 모든 값을 해제함. 모든 원소를 순차적으로 전부 확인함.
(DESTROY-ONE-VALUE<Indices>(), ...);
// (2) Indices...를 열거하면서 동적 인덱스 변수 `_dataPlace`와 컴파일 상수 인덱스 `Indices`를 비교
// (2-1) 두 인덱스가 다르면 할당 (std::construct_at)
// (2-2) 두 인덱스가 같으면 대입 (=)
(CONSTRUCT-OR-ASSIGN-ONE-VALUE<Indices>(), ...);
}
상기 코드는 내부 구현 함수의 개형이다. 이제 방문자 패턴의 구현을 알아보자.9.3.2.1. 방문자 패턴
방문자 패턴이란 특정 사례[4]에 대한 함수가 주어졌다고 해보자. 그리고 모든 원소를 순회하면서 이 함수를 실행할 수 있는지 확인될 때마다 실행하는 형태가 방문자 패턴이다. 다만 우리는 인덱스만 있어도 자료형 리스트 덕분에 자료형 등등 메타 정보를 알 수 있으므로 컴파일 상수만 쓰면 된다. 멤버 함수를 더 만들 수도 있는데, 예전 템플릿 문서대로 람다식을 써보자.#!syntax cpp
template<typename U, typename V, index_type... Indices>
constexpr void _SetValue(std::integer_sequence<index_type, Indices...>, std::in_place_type_t<U>, V&& value);
{
// (1) Indices...를 열거하면서 모든 값을 해제함. 비교하지 않고 공평하게 전부 해제함.
auto destroy_iterator = [&]<index_type I>(std::integral_constant<index_type, I>);
// 방문자 함수
auto destroy_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>);
destroy_visitor(...);
// 값 할당
}
개형은 위와 같다. 왜 메모리 해제를 먼저 해줘야 하냐면 공용체의 값이 겹쳐져 있기 때문이다. 만약 들어있는 값이 포인터인데 삭제하면서 nullptr
로 초기화한다면, 그 포인터와 같은 메모리 위치에 있는 값들도 전부 0
으로 바껴버린다. 그래서 삭제를 위해 순회하는 도중에 할당할 자료형을 만나더라도 할당할 수가 없다.#!syntax cpp
auto destroy_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
// 변수와 컴파일 상수 비교
if (_dataPlace == I)
{
auto& current_value = _GetValue(std::in_place_type<At_t<mirror_t, I>>);
// std::destroy_at(T*): 표준 라이브러리의 메모리 해제 함수
// 소멸자를 호출한다.
std::destroy_at(std::addressof(current_value));
}
};
auto destroy_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
// 템플릿 매개변수 묶음 확장
(destroy_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
방문자 함수의 구현 내용은 위와 같다. destroy_visitor
에서 모든 원소를 순회하면서 현재 원소를 삭제한다.#!syntax cpp
template<typename U, index_type I>
constexpr void _SetValue(std::in_place_type_t<U>, V&& value)
{
// 기존 값 삭제
// `Size`는 `T, Ts...`의 길이
if (_hasValue)
{
destroy_visitor(std::make_integer_sequence<index_type, Size>{});
}
_GetValue(std::in_place_type_t<U>) = std::forward<V>(value);
_hasValue = true;
_dataPlace = FindIndex_v<mirror_t, U>;
}
실제로 작동하며 당장의 사용에는 문제는 없다. 상기 코드는 논리적으로는 맞지만 오류 관련한 문제가 있다. 만약 예외가 메모리 해제 전후, 생성 전후에 일어나면 어떻게 될까? 그럼 예외 때문에 상태가 어그러지면서 정상적인 공용체가 아니게 된다. 오직 try
catch
구문으로 예외를 확인하고 클래스 외부에서 수동으로 공용체를 수정해야 하는데 그럼 내부 멤버를 public
으로 공개할 수도 있어야 한다는 말이고 이는 안전하지 않은 코딩을 권장하는 꼴 밖에 안된다. 또한 예외를 어찌어찌 처리해도 _hasValue
같은 내부 플래그가 갱신되지 않기 때문에 문제가 생겼는지 파악할 수가 없다. 안전하지 않은 코드에서 벗어나기 위해 여기까지 왔는데 그럴 수는 없다. 다행히 거창하게 말한 것 치고는 해결 방법은 간단하다. 소멸자를 사용한 RAII 패턴을 구현하면 된다.#!syntax cpp
constexpr void _SetValue(...)
{
// (1) RAII를 위한 구조체와 인스턴스 정의
// 반드시 noexcept 생성자와 멤버 함수가 있어야 함.
struct Failsafe
{
index_type& refDataPlace;
bool& refHasValue;
U** ptr = nullptr;
// 예외가 발생하면 nullptr로 만들어야 함.
bool isSafe = false;
};
Failsafe failsafe{ .refDataPlace = _dataPlace, .refHasValue = _hasValue };
// (2) 내부 플래그 백업
// (3) 값이 없음으로 내부 플래그 설정
_hasValue = false;
_dataPlace = -1;
// (4) 기존의 값 해제
// 예외가 발생할 수 있음.
destroy_visitor(...);
// (3) 새로운 값 할당
// 예외가 발생할 수 있음.
failsafe.ptr = std::addressof(...);
// (4) RAII 구조체 파괴
// `Failsafe`의 소멸자가 호출됨. 성공 여부에 따라 내부 값들을 설정함.
failsafe.isSafe = true;
return;
}
RAII 패턴은 특정 구조체에 플래그와 예외 발생 시 잔여값 처리를 맡기는 식으로 동작한다. 가령 데이터 멤버에 bool
플래그를 두고 모든 처리가 끝나면 플래그를 안전하다고 설정하면 된다. 이런식으로 구현하면 예외가 있어도 공용체를 안전하게 이용할 수 있다.#!syntax cpp
template<typename U, typename V>
constexpr void _SetValue(std::in_place_type_t<U>, V &&value)
{
struct Failsafe
{
bool& refHasValue;
index_type& refDataPlace;
U** targetPtr = nullptr;
bool isSafe = false;
constexpr ~Failsafe() noexcept
{
refHasValue = isSafe;
if (!isSafe)
{
refDataPlace = discriminatedUnionOutbound;
*targetPtr = nullptr;
}
else
{
refDataPlace = FindIndex_v<mirror_t, U, index_type>;
}
}
};
Failsafe failsafe{ .refHasValue = _hasValue, .refDataPlace = _dataPlace };
// 플래그 백업
const bool has_value = _hasValue;
U* target_ptr = nullptr;
// 값을 가지고 있지 않다고 표시
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
if (has_value)
{
auto destroy_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
auto& current_value = _GetValue(std::in_place_type<At_t<mirror_t, I>>);
if (_dataPlace == I)
{
std::destroy_at(std::addressof(current_value));
}
if constexpr (std::is_same_v<At_t<mirror_t, I>, U>)
{
// 순회하면서 미리 할당 위치를 찾음.
target_ptr = std::addressof(current_value);
}
};
auto destroy_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(destroy_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
destroy_visitor(std::make_integer_sequence<index_type, Size>{});
}
else
{
auto& current_value = _GetValue(std::in_place_type<U>);
target_ptr = std::addressof(current_value);
}
failsafe.targetPtr = std::addressof(target_ptr);
failsafe.isSafe = (std::construct_at(target_ptr, std::forward<V>(value)) != nullptr);
}
모든 상황을 고려한 _SetValue
의 구현은 위와 같다.#!syntax cpp
template<typename U, typename... Args>
constexpr U& _Emplace(std::in_place_type_t<U>, Args&&... args)
{
struct Failsafe
{
bool& refHasValue;
index_type& refDataPlace;
U** targetPtr = nullptr;
bool isSafe = false;
constexpr ~Failsafe() noexcept
{
refHasValue = isSafe;
if (!isSafe)
{
refDataPlace = discriminatedUnionOutbound;
*targetPtr = nullptr;
}
else
{
refDataPlace = FindIndex_v<mirror_t, U, index_type>;
}
}
};
Failsafe failsafe{ .refHasValue = _hasValue, .refDataPlace = _dataPlace };
const bool has_value = _hasValue;
U* target_ptr = nullptr;
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
if (has_value)
{
auto destroy_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
auto& current_value = _GetValue(std::in_place_type<At_t<mirror_t, I>>);
if (_dataPlace == I)
{
std::destroy_at(std::addressof(current_value));
}
if constexpr (std::is_same_v<At_t<mirror_t, I>, U>)
{
target_ptr = std::addressof(current_value);
}
};
auto destroy_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(destroy_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
destroy_visitor(std::make_integer_sequence<index_type, Size>{});
}
else
{
auto& current_value = _GetValue(std::in_place_type<U>);
target_ptr = std::addressof(current_value);
}
failsafe.targetPtr = std::addressof(target_ptr);
failsafe.isSafe = (std::construct_at(target_ptr, std::forward<Args>(args)...) != nullptr);
return *target_ptr;
}
template<typename U, typename V>
constexpr void _SetValue(std::in_place_type_t<U>, V &&value)
{
(void) _Emplace(std::in_place_type<U>, std::forward<V>(value));
}
_Emplace
도 똑같은 방식으로 정의할 수 있다. _SetValue
의 구현을 _Emplace
를 쓰도록 고칠 수 있다.9.3.2.2. 동등 비교 연산자
#!syntax cpp
[[nodiscard]] constexpr bool operator==(const DUBase&) noexcept;
다음은 동등 비교 연산자를 구현해보자. 이 연산자도 자동으로 생성되지 않기 때문에 직접 구현해줘야 한다. 마찬가지로 방문자 패턴을 활용하면 된다.#!syntax cpp
constexpr bool operator==(const DUBase& rhs) noexcept
{
if (this == std::addressof(rhs))
{
return true;
}
else if (_hasValue and rhs._hasValue and _dataPlace == rhs._dataPlace)
{
// 조건식을 어떻게?
// 여기에서 방문자 패턴 사용
return _GetValue<?>() == rhs._GetValue<?>();
}
else
{
return false;
}
}
대략적인 개형은 위와 같다. 방문자 패턴으로 비교식만 작성해보자. 참고할 점은 비교하려는 두 공용체의 번호가 같으므로 하나의 함수에서 처리할 수 있다.#!syntax cpp
constexpr bool operator==(const DUBase& rhs) const noexcept
{
if (this == nullptr)
{
return false;
}
else if (this == std::addressof(rhs))
{
return true;
}
else if (_hasValue and rhs._hasValue and _dataPlace == rhs._dataPlace)
{
auto cmp_iterator = [&]<index_type I>(std::integral_constant<index_type, I>) -> int
{
using V = At_t<mirror_t, I>;
if (_dataPlace == I)
{
const auto& lhs_v = _GetValue(std::in_place_type<V>);
const auto& rhs_v = rhs._GetValue(std::in_place_type<V>);
if constexpr (std::equality_comparable<V>) // 검사하는 대신에 컴파일 오류를 발생시켜도 됨.
{
return (lhs_v == rhs_v) ? 1 : 0;
}
else
{
return 0;
}
}
else
{
return 0;
}
};
auto cmp_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>) -> bool
{
const int sum = (0 + ... + cmp_iterator(std::integral_constant<index_type, Indices>{}));
return 0 != sum;
};
return cmp_visitor(std::make_integer_sequence<index_type, Size>{});
}
else
{
return false;
}
}
동등 비교 연산자의 구현 내용은 위와 같다.9.3.2.3. 복사 대입 연산자
#!syntax cpp
constexpr DUBase& operator=(const DUBase&) noexcept(NothrowCopyables<Ts...> and NothrowCopyAssignables<Ts...>) requires(std::is_copy_assignable_v<Ts> && ...);
복사 대입 연산자는 이전에 구현했던 _Emplace
처럼 구현하면 된다.#!syntax cpp
constexpr DUBase& operator=(const DUBase& rhs)
{
// (1) 두 클래스 모두 값을 가지고 있지 않으면 아무것도 안함.
// (2) 두 클래스가 값을 가지고, 두 인덱스가 같은 상황을 판별함.
// (3-A1) 두 인덱스가 서로 같은 경우, `this`의 내부 플래그를 값 없음으로 설정하고 메모리를 해제함. `this`에 활성화된 값이 없으면 아무것도 안함.
// (3-A2) `this`의 값에 `rhs`의 값을 대입함. `rhs`에 활성화된 값이 없으면 아무것도 안함.
// (3-A3) RAII를 써서 성공 여부를 판별함.
// (3-B1) 두 인덱스가 서로 다른 경우, `this`의 내부 플래그를 값 없음으로 설정하고 메모리를 해제함. `this`에 활성화된 값이 없으면 아무것도 안함.
// (3-B2) `rhs`의 값의 위치를 찾아냄.
// (3-B3) `this`의 원소를 방문하면서 `rhs`의 인덱스와 같은 원소를 찾아냄.
// (3-B4) `this`의 값에 `rhs`의 값을 대입함. `rhs`에 활성화된 값이 없으면 아무것도 안함.
// (3-B5) RAII를 써서 성공 여부를 판별함.
return *this;
}
대략적인 개요는 위와 같다.#!syntax cpp
constexpr DUBase& operator=(const DUBase& rhs)
{
if (this == std::addressof(rhs))
{
return *this;
}
const bool lhas = _hasValue;
const bool rhas = rhs._hasValue;
if (!lhas and !rhas)
{
return *this;
}
const index_type lindex = _dataPlace;
const index_type rindex = rhs._dataPlace;
if (lhas and (!rhas or (lindex != rindex)))
{
// `this`의 값을 해제
auto destroy_me_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
if (I == lindex)
{
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
using V = At_t<mirror_t, I>;
auto& my_value = _GetValue(std::in_place_type<V>);
// `rhs`의 값을 복사 대입
std::destroy_at(std::addressof(my_value));
}
};
auto destroy_me_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(destroy_me_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
destroy_me_visitor(std::make_integer_sequence<index_type, Size>{});
}
if (rhas and lindex == rindex)
{
// `rhs`의 값을 복사 대입
auto copy_for_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
if (I == lindex)
{
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
using V = At_t<mirror_t, I>;
auto& my_value = _GetValue(std::in_place_type<V>);
auto& yr_value = rhs._GetValue(std::in_place_type<V>);
my_value = yr_value;
_hasValue = true;
_dataPlace = rindex;
}
};
auto copy_for_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(copy_for_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
copy_for_visitor(std::make_integer_sequence<index_type, Size>{});
}
else if (rhas)
{
// `rhs`의 값으로 복사 생성
auto copy_ctr_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
if (I == rindex)
{
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
using V = At_t<mirror_t, I>;
auto& my_value = _GetValue(std::in_place_type<V>);
auto& yr_value = rhs._GetValue(std::in_place_type<V>);
auto* my_ptr = std::construct_at(std::addressof(my_value), yr_value);
if (nullptr != my_ptr)
{
_hasValue = true;
_dataPlace = rindex;
}
}
};
auto copy_ctr_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(copy_ctr_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
copy_ctr_visitor(std::make_integer_sequence<index_type, Size>{});
}
return *this;
}
구현 내용은 위와 같다. 대입하는 공용체 `rhs`
에 활성화된 값이 없으면 대입되는 공용체의 메모리를 해제하고 종료한다.9.3.2.4. 이동 대입 연산자
#!syntax cpp
constexpr DUBase& operator=(DUBase&&) noexcept(NothrowMovables<Ts...> and NothrowMoveAssignables<Ts...>);
이동 대입 연산자는 복사 대입 연산자와 비슷하지만 조금 더 복잡하다. 이동된 클래스의 메모리를 전부 해제해줘야 하기 때문이다.이때 구현 방향을 두가지로 잡을 수 있다. 첫번째는 대입하는 공용체
`rhs`
에 값이 없으면 대입되는 공용체도 비우는 것. 두번째는 `rhs`
에 값이 없으면 아무것도 하지 않는 것이다. 논리는 첫번째가 맞으며 다만 두번째 경우를 TryAssign
따위의 별도의 함수를 만들어도 좋다.#!syntax cpp
constexpr DUBase& operator=(DUBase&& rhs)
{
// (1) 두 클래스 모두 값을 가지고 있지 않으면 아무것도 안함.
// (2) `rhs`가 값을 가지고 있지 않으면 `this`의 내부 플래그를 값 없음으로 설정하고 메모리를 해제함. `this`에도 활성화된 값이 없으면 아무것도 안함.
// (3-A1) 두 인덱스가 서로 같은 경우, `this`와 `rhs`의 내부 플래그를 값 없음으로 설정함.
// (3-A2) `this`의 값에 `rhs` 값을 이동시킴.
// (3-A3) `this`의 내부 플래그를 값이 있다고 설정함.
// (3-A4) `rhs`의 메모리를 해제함.
// (3-B1) 두 인덱스가 서로 다른 경우, `this`와 `rhs`의 내부 플래그를 값 없음으로 설정함.
// (3-B2) `this`의 원소를 방문하면서 `rhs`의 인덱스와 같은 원소를 찾아냄.
// (3-B3) `this`에 `rhs` 값을 이동 생성함.
// (3-B4) `this`의 내부 플래그를 값이 있다고 설정함.
// (3-B5) `rhs`의 메모리를 해제함.
return *this;
}
대략적인 개요는 위와 같다.#!syntax cpp
constexpr DUBase& operator=(DUBase&& rhs)
{
if (this == std::addressof(rhs))
{
return *this;
}
const bool lhas = _hasValue;
const bool rhas = rhs._hasValue;
const index_type lindex = _dataPlace;
const index_type rindex = rhs._dataPlace;
if (lhas and (!rhas or (lindex != rindex)))
{
auto destroy_me_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
if (I == lindex)
{
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
using V = At_t<mirror_t, I>;
auto& my_value = _GetValue(std::in_place_type<V>);
std::destroy_at(std::addressof(my_value));
}
};
auto destroy_me_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(destroy_me_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
destroy_me_visitor(std::make_integer_sequence<index_type, Size>{});
}
if (rhas and lindex == rindex)
{
// `rhs`의 값을 이동 대입
auto move_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
if (I == lindex)
{
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
rhs._hasValue = false;
rhs._dataPlace = discriminatedUnionOutbound;
using V = At_t<mirror_t, I>;
auto& my_value = _GetValue(std::in_place_type<V>);
auto& yr_value = rhs._GetValue(std::in_place_type<V>);
my_value = std::move(yr_value);
_hasValue = true;
_dataPlace = rindex;
// 마지막에 정리
std::destroy_at(std::addressof(yr_value));
}
};
auto move_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(move_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
move_visitor(std::make_integer_sequence<index_type, Size>{});
}
else if (rhas)
{
// `rhs`의 값으로 이동 생성
auto move_ctr_iterator = [&]<index_type I>(std::integral_constant<index_type, I>)
{
if (I == rindex)
{
_hasValue = false;
_dataPlace = discriminatedUnionOutbound;
rhs._hasValue = false;
rhs._dataPlace = discriminatedUnionOutbound;
using V = At_t<mirror_t, I>;
auto& my_value = _GetValue(std::in_place_type<V>);
auto& yr_value = rhs._GetValue(std::in_place_type<V>);
auto* my_ptr = std::construct_at(std::addressof(my_value), std::move(yr_value));
if (nullptr != my_ptr)
{
_hasValue = true;
_dataPlace = rindex;
}
// 마지막에 정리
std::destroy_at(std::addressof(yr_value));
}
};
auto move_ctr_visitor = [&]<index_type... Indices>(std::integer_sequence<index_type, Indices...>)
{
(move_ctr_iterator(std::integral_constant<index_type, Indices>{}), ...);
};
move_ctr_visitor(std::make_integer_sequence<index_type, Size>{});
}
return *this;
}
구현은 위와 같다. 이동된 공용체는 값을 비운다.9.3.2.5. _GetValue 최적화
앞서 구현한 값을 얻어오는 함수_GetValue
를 최적화해보자. 가장 많이 쓸 함수인데 재귀 함수라서 성능 문제가 있다. 스택 오버플로우가 일어날 수도 있고.먼저 언급할 사항은 우리가 저장소 클래스
DUStorage
의 데이터 멤버 tail
을 연속으로 접근하는건 피할 수 없는 상황이란 것이다. 어쩔 수 없이 tail.tail.tail....
이런 식으로 하위 공용체에 접근해야만 한다. 여기서 할 수 있는 최선의 일은 재귀 회수를 줄이는 것이다.- <C++ 예제 보기>
#!syntax cpp // 멤버 함수 `_GetValueImpl` template<typename Self, typename U, typename Current> constexpr auto& _GetValueImpl(this Self&& self, std::in_place_type_t<U>, Current& node) noexcept { if constexpr (std::is_same_v<typename Current::value_type, U>) { return node.value; } else { return self._GetValueImpl(std::in_place_type<U>, node.tail); } } // 멤버 함수 `_GetValue` template<typename Self, typename U> constexpr auto& _GetValue(this Self&& self, std::in_place_type_t<U>) noexcept { constexpr index_type place = FindIndex_v<mirror_t, U, index_type>; if constexpr (place == 0) { return self._data.value; } else if constexpr (place == 1) { return self._data.tail.value; } else if constexpr (place == 2) { return self._data.tail.tail.value; } else if constexpr (place == 3) { return self._data.tail.tail.tail.value; } else if constexpr (place == 4) { return self._data.tail.tail.tail.tail.value; } else if constexpr (place == 5) { return self._data.tail.tail.tail.tail.tail.value; } else if constexpr (place == 6) { return self._data.tail.tail.tail.tail.tail.tail.value; } else if constexpr (place == 7) { return self._data.tail.tail.tail.tail.tail.tail.tail.value; } else if constexpr (place == 8) { return self._data.tail.tail.tail.tail.tail.tail.tail.tail.value; } else { return self._GetValueImpl(std::in_place_type<U>, self._data); } }
if constexpr
을 사용했기에 8 이하의 인덱스에선 성능 문제가 없다. 만약 큰 인덱스를 원한다면 이런식으로 조건문을 만들거나 매크로로 이 과정을 간소화시킬 수 있다.9.3.3. 구별되는 공용체 클래스
- <C++ 예제 보기>
#!syntax cpp template<typename... Ts> class DiscriminatedUnion : private DUBase<Ts...> { public: using mirror_t = TypeList<Ts...>; static_assert(std::is_same_v<Unique_t<mirror_t>, mirror_t>, "중복되는 자료형은 사용하실 수 없습니다!"); using base = DUBase<Ts...>; using type = DiscriminatedUnion<Ts...>; using index_type = base::index_type; using size_type = base::size_type; template<index_type Index> using element_t = At_t<mirror_t, Index>; static constexpr size_type Size = 1 + sizeof...(Ts); // 공란 초기화 explicit constexpr DiscriminatedUnion() noexcept : base() {} template<typename U, typename... Args> requires (Has_v<mirror_t, U> and std::constructible_from<U, Args...>) constexpr DiscriminatedUnion(std::in_place_type_t<U>, Args&&... args) noexcept(Has_v<mirror_t, U> and NothrowConstructible<U, Args...>) : base(std::in_place_type<U>, std::forward<Args>(args)...) {} template<index_type Index, typename... Args> requires (0 <= Index and Index < Size) constexpr DiscriminatedUnion(std::in_place_index_t<Index>, Args&&... args) noexcept(0 <= Index and Index < Size and NothrowConstructible<element_t<Index>, Args...>) : DiscriminatedUnion(std::in_place_type<element_t<Index>>, std::forward<Args>(args)...) {} template<typename U, typename V> requires (Has_v<mirror_t, U> and std::assignable_from<U &, V &&>) constexpr DiscriminatedUnion& SetValue(std::in_place_type_t<U>, V &&value) noexcept(Has_v<mirror_t, U> and NothrowAssignables<U, V>) { base::_SetValue(std::in_place_type<U>, std::forward<V>(value)); return *this; } template<typename U, typename V> requires (Has_v<mirror_t, U> and std::assignable_from<U &, V &&>) constexpr DiscriminatedUnion& SetValue(V &&value) noexcept(Has_v<mirror_t, U> and NothrowAssignables<U, V>) { return SetValue(std::in_place_type<U>, std::forward<V>(value)); } template<index_type Index, typename V> requires (0 <= Index and Index < Size and std::assignable_from<element_t<Index> &, V &&>) constexpr DiscriminatedUnion& SetValue(std::in_place_index_t<Index>, V &&value) noexcept(0 <= Index and Index < Size and NothrowAssignables<element_t<Index>, V>) { return SetValue(std::in_place_type<element_t<Index>>, std::forward<V>(value)); } template<index_type Index, typename V> requires (0 <= Index and Index < Size and std::assignable_from<element_t<Index> &, V &&>) constexpr DiscriminatedUnion& SetValue(V &&value) noexcept(0 <= Index and Index < Size and NothrowAssignables<element_t<Index>, V>) { return SetValue(std::in_place_type<element_t<Index>>, std::forward<V>(value)); } template<typename U, typename... Args> requires (Has_v<mirror_t, U> and std::constructible_from<U, Args...>) constexpr auto& Emplace(Args&&... args) noexcept(Has_v<mirror_t, U> and NothrowConstructible<U, Args...>) { return base::_Emplace(std::in_place_type<U>, std::forward<Args>(args)...); } template<index_type Index, typename... Args> requires (0 <= Index and Index < Size and std::constructible_from<element_t<Index>, Args...>) constexpr auto& Emplace(Args&&... args) noexcept(0 <= Index and Index < Size and NothrowConstructible<element_t<Index>, Args...>) { return base::_Emplace(std::in_place_type<element_t<Index>>, std::forward<Args>(args)...); } template<typename U, typename Self> requires (Has_v<mirror_t, U>) [[nodiscard]] constexpr decltype(auto) GetValue(this Self&& self, std::in_place_type_t<U>) noexcept(Has_v<mirror_t, U>) { return std::forward_like<Self>(self._GetValue(std::in_place_type<U>)); } template<typename U, typename Self> requires (Has_v<mirror_t, U>) [[nodiscard]] constexpr decltype(auto) GetValue(this Self&& self) noexcept(Has_v<mirror_t, U>) { return std::forward<Self>(self).GetValue(std::in_place_type<U>); } template<index_type Index, typename Self> requires (0 <= Index and Index < Size) [[nodiscard]] constexpr decltype(auto) GetValue(this Self&& self, std::in_place_index_t<Index>) noexcept(0 <= Index and Index < Size) { return std::forward<Self>(self).GetValue(std::in_place_type<element_t<Index>>); } template<index_type Index, typename Self> requires (0 <= Index and Index < Size) [[nodiscard]] constexpr decltype(auto) GetValue(this Self&& self) noexcept(0 <= Index and Index < Size) { return std::forward<Self>(self).GetValue(std::in_place_type<element_t<Index>>); } template<typename U> constexpr bool TryDestroy(std::in_place_type_t<U>) noexcept(Has_v<mirror_t, U> and NothrowDestructible<U>) { if constexpr (Has_v<mirror_t, U>) { if (Contains(std::in_place_type<U>)) { std::destroy_at(std::addressof(GetValue(std::in_place_type<U>))); return true; } else { return false; } } else { return false; } } template<index_type Index> requires (0 <= Index and Index < Size) constexpr bool TryDestroy(std::in_place_index_t<Index>) noexcept(Index < Size and NothrowDestructible<element_t<Index>>) { if constexpr (Index < Size) { if (Contains(std::in_place_index<Index>)) { std::destroy_at(std::addressof(GetValue(std::in_place_index<Index>))); return true; } else { return false; } } else { return false; } } template<typename U> constexpr bool TryDestroy() noexcept(Has_v<mirror_t, U> and NothrowDestructible<U>) { return TryDestroy(std::in_place_type<U>); } template<index_type Index> requires (0 <= Index and Index < Size) constexpr bool TryDestroy() noexcept(Index < Size &&NothrowDestructible<element_t<Index>>) { return TryDestroy(std::in_place_index<Index>); } [[nodiscard]] constexpr index_type GetIndex() const noexcept { return base::_GetIndex(); } [[nodiscard]] constexpr bool HasValue() const noexcept { return base::_HasValue() && GetIndex() != discriminatedUnionOutbound; } [[nodiscard]] constexpr bool HasNoValueByException() const noexcept { return HasValue() && GetIndex() == discriminatedUnionOutbound; } template<typename U> [[nodiscard]] constexpr bool Contains(std::in_place_type_t<U>) const noexcept { return base::_Contains(std::in_place_type<U>); } template<index_type Index> requires (0 <= Index and Index < Size) [[nodiscard]] constexpr bool Contains(std::in_place_index_t<Index>) const noexcept { return base::_Contains(std::in_place_index<Index>); } template<typename U> [[nodiscard]] constexpr bool Contains() const noexcept { return base::_Contains(std::in_place_type<U>); } template<index_type Index> requires (0 <= Index and Index < Size) [[nodiscard]] constexpr bool Contains() const noexcept { return base::_Contains(std::in_place_index<Index>); } using base::CanHold; constexpr DiscriminatedUnion& operator=(const DiscriminatedUnion&) noexcept(NothrowCopyables<Ts...> and NothrowCopyAssignables<Ts...>) = default; constexpr DiscriminatedUnion& operator=(DiscriminatedUnion&&) noexcept(NothrowMovables<Ts...> and NothrowMoveAssignables<Ts...>) = default; [[nodiscard]] constexpr bool operator==(const DiscriminatedUnion& other) const noexcept = default; };
std::forward_like
로 완벽한 전달을 수행한다. std::forward_like<U, T>(T&&)
는 값 `T`
를 `U`
의 값 범주와 동일하게 전달해주는 함수다.9.4. 결론
#!syntax cpp
struct Empty { explicit Empty() = default; };
int main()
{
using union_t0 = DiscriminatedUnion<long, unsigned char, int, Empty, bool, short, float>;
using union_t1 = DiscriminatedUnion<int, std::string, double>;
constexpr union_t0 tvariant0;
constexpr union_t0 tvariant1{};
std::println("Is tvariant0 is same with tvariant1? '{}'.", tvariant0 == tvariant1);
constexpr union_t0 tvariant2(std::in_place_index<6>, 80000.01234f);
constexpr union_t0 tvariant3(std::in_place_type<short>, 20);
std::println("Is tvariant2 is same with tvariant3? '{}'.", tvariant2 == tvariant3);
const auto tvar2_v0 = tvariant2.GetValue<bool>();
const auto tvar2_v1 = tvariant2.GetValue<short>();
const auto tvar2_v2 = tvariant2.GetValue<int>();
const auto tvar2_v3 = tvariant2.GetValue<unsigned char>();
const auto tvar2_v4 = tvariant2.GetValue<long>();
const auto tvar2_v5 = tvariant2.GetValue<float>();
decltype(auto) right_float_ref = tvariant2.GetValue<float>();
std::println("right_float_ref is '{}'.\n", right_float_ref);
const auto tvar3_v0 = tvariant3.GetValue<bool>();
const auto tvar3_v1 = tvariant3.GetValue<short>();
const auto tvar3_v2 = tvariant3.GetValue<int>();
const auto tvar3_v3 = tvariant3.GetValue<unsigned char>();
const auto tvar3_v4 = tvariant3.GetValue<long>();
const auto tvar3_v5 = tvariant3.GetValue<float>();
constexpr union_t0 tvariant4(std::in_place_type<int>, 100);
constexpr union_t0 tvariant5(std::in_place_type<int>, 100);
std::println("tvariant4 is '{}'.", tvariant4.GetValue<int>());
std::println("tvariant5 is '{}'.", tvariant5.GetValue<int>());
std::println("Is tvariant4 is same with tvariant5? '{}'.\n", tvariant4 == tvariant5);
union_t0 tvariant6(std::in_place_type<unsigned char>, 'B');
std::println("(1) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
tvariant6.SetValue(std::in_place_type<int>, 30);
tvariant6.SetValue(std::in_place_index<0>, 60);
std::println("(2) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
tvariant6.Emplace<6>(45);
std::println("(3) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
tvariant6.SetValue(std::in_place_index<0>, 500);
std::println("(4) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
tvariant6.SetValue(std::in_place_index<4>, 0);
std::println("(5) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
tvariant6.SetValue(std::in_place_index<1>, 250);
std::println("(6) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
tvariant6.SetValue(std::in_place_index<3>, Empty{});;
std::println("(7) The index of tvariant6 is '{}'.", tvariant6.GetIndex());
auto& bref = tvariant6.Emplace<5>(-30);
bref = 70;
bref -= 20;
std::println("bref is '{}'.\n", bref);
union_t1 tvariant7{ std::in_place_index<1>, "Hello, world!" };
std::println("(1) tvariant7 is '{}'.", tvariant7.GetValue<1>());
tvariant7.Emplace<1>("AABB");
std::println("(2) tvariant7 is '{}'.", tvariant7.GetValue(std::in_place_type<std::string>));
tvariant7.Emplace<std::string>("BBCC");
union_t1 tvariant8{ std::in_place_index<1>, "Bye, world!" };
std::println("(3) tvariant7 is '{}'.", tvariant7.GetValue<std::string>());
std::println("(3) tvariant8 is '{}'.", tvariant8.GetValue<std::string>());
std::println("Is tvariant7 is same with tvariant8? '{}'.\n", tvariant7 == tvariant8);
tvariant7 = tvariant8;
tvariant8 = union_t1{ std::in_place_index<1>, "CCDD" };
std::println("(4) tvariant7 is '{}'.", tvariant7.GetValue<1>());
std::println("(4) tvariant8 is '{}'.", tvariant8.GetValue<1>());
std::println("Is tvariant7 is same with tvariant8? '{}'.\n", tvariant7 == tvariant8);
//tvariant7.Emplace(std::in_place_type<std::string>, "CCDD");
tvariant7.Emplace<std::string>("CCDD");
std::println("(5) tvariant7 is '{}'.", tvariant7.GetValue(std::in_place_index<1>));
std::println("(5) tvariant8 is '{}'.", tvariant8.GetValue(std::in_place_type<std::string>));
std::println("Is tvariant7 is same with tvariant8? '{}'.\n", tvariant7 == tvariant8);
}
실행 결과는 위와 같다.[1] C++26에서 멤버의 포인터로 활성화 여부를 확인하는 메타 함수가 추가되었다[2] 동등 비교(
==
) 연산자는 생성된다[3] std::is_trivially_destructible_v<NonTrivialUnion>가 true이므로[4] 함수 오버로딩 등