Reference
- Ch 25, C++ Templates The Complete Guide
Contents
- Basic Tuple Design
- Basic Tuple Operations
- Tuple Algorithms
- Expanding Tuples
- Optimizing Tuple
튜플은 실행 프로그램 내의 타입 리스트가 표현된 것이라고 볼 수 있다. 예를 들어, 타입 리스트 Typelist<int, double, std::string>이 컴파일 시간에 조작할 수 있는 int, double, std::string을 가진 타입의 시퀀스를 나타내는 것잉라면 Tuple<int, double, std::string>은 실행 시간에 조작할 수 있는 int, double, std::string에 대한 저장 공간을 나타낸다.
이번 포스팅에서는 C++11에 도입된 std::tuple과 유사한 동작하도록 간략한 버전의 Tuple을 직접 구현해보자. 튜플을 구현할 때 사용되는 타입 리스트 알고리즘은 아래 포스팅에서 다루고 있으며, 이 포스팅에서는 따로 다루지는 않는다.
Basic Tuple Design
Storage
튜플은 템플릿 인자 리스트 내에 있는 각 타입에 대한 저장 공간을 가지고 있다. std::tuple의 경우, 이 템플릿 t의 저장 공간을 함수 템플릿 std::get을 사용하여 std::get<I>(t)와 같은 방식으로 액세스할 수 있다.
재귀 방식으로 구현한 튜플은 N(>0)개의 요소를 가진 튜플이 단일 요소(첫 번째 요소)와 나머지 N-1개의 요소를 포함한 튜플로 저장할 수 있다는 아이디어를 활용한다. 당연히 요소가 하나도 없는 튜플에 대해서는 특별하게 취급한다. 따라서, 요소가 3개인 튜플인 Tuple<int, double, std::string>은 int와 Tuple<double, std::string>을 저장한다. 요소가 2개인 튜플 Tuple<double, std::string>은 double과 Tuple<std::string>을 저장하고, 마지막 튜플 Tuple<std::string>은 std::string과 Tuple<>을 저장한다. 이는 타입 리스트 알고리즘에서 사용한 재귀적 구현과 같은 방식이다.
template<typename... Types>
class Tuple;
// recursive case
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
{
private:
Head head;
Tuple<Tail...> tail;
public:
//constructor
Tuple() {}
Tuple(Head const& head, Tuple<Tail...> const& tail) : head(head), tail(tail) {}
// ...
Head& getHead() { return head; }
Head const& getHead() const { return head; }
Tuple<Tail...>& getTail() { return tail; }
Tuple<Tail...> const& getTail() const { return tail; }
};
// basic case
template<>
class Tuple<> {}; // no storage for empty tuple
재귀 구현에서 각 Tuple 인스턴스에는 리스트 내 첫 번째 요소를 저장하는 데이터 멤버인 head와 나머지 요소를 저장하는 데이터 멤버 tail이 있다. 기본 구현은 비어 있는 튜플이므로 별도의 저장 공간은 없다.
튜플의 각 요소에 액세스하기 위한 get 함수 템플릿은 요청된 요소를 추출하기 위해 튜플의 재귀 구조를 따라 들어가게 된다.
// recursive case
template<unsigned N>
struct TupleGet {
template<typename Head, typename... Tail>
static auto const& apply(Tuple<Head, Tail...> const& t) {
return TupleGet<N-1>::apply(t.getTail());
}
};
// basic case
template<>
struct TupleGet<0> {
template<typename Head, typename... Tail>
static Head const& apply(Tuple<Head, Tail...> const& t) {
return t.getHead();
}
};
template<unsigned N, typename... Types>
auto const& get(Tuple<Types...> const& t) {
return TupleGet<N>::apply(t);
}
get 함수 템플릿은 TupleGet의 정적 멤버 함수를 호출하는 래퍼이다. 이는 함수 템플릿에서는 부분 특수화가 없다는 점을 극복하기 위한 차선책이다. 이 기법을 사용하면 N의 값에 대해 특수화할 수 있다.
Construction
위와 같이 구현된 튜플의 생성자로도 유용하지만, 독립적인 값 집합이나 다른 튜플로부터 튜플을 생성할 수 있으면 더욱 좋다.
Tuple() {}
Tuple(Head const& head, Tuple<Tail...> const& tail) : head(head), tail(tail) {}
위 구현만으로는 독립적인 값 집합으로부터 복사 생성하는 것은 불가능하다. 아래 코드를 컴파일해보면 에러가 발생하는 것을 확인할 수 있다.
Tuple<int, double, std::string> t(17, 3.14, "Hello, World!");
위 코드가 정상적으로 컴파일되려면 아래의 생성자가 필요하다.
Tuple(Head const& head, Tail const&... tail) : head(head), tail(tail...) {}
하지만, 이것만으로 충분하지는 않다. 사용자는 요소의 일부 값(전체가 아닌)을 초기화할 때 이동 생성하기를 원할 수도 있고, 다양한 타입의 값에서 생성된 요소를 갖기를 원할 수도 있다. 따라서 튜플을 초기화하려면 perfect forwarding을 사용해야 한다.
template<typename VHead, typename... VTail>
Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
그리고 아래 생성자를 통해 다른 튜플로부터 튜플을 생성할 수 있다.
template<typename VHead, typename... VTail>
Tuple(Tuple<VHead, VTail...> const& other) : head(other.getHead()), tail(other.getTail()) {}
하지만 위의 생성자로는 튜플 변환에 충분하지 않다. 위의 구현들로부터 아래의 코드는 컴파일 에러를 발생시킨다.
// error: no conversion from Tuple<int, double, string> to long
Tuple<long int, long double, std::string> t2(t);
위 에러의 원인은 독립적인 값 집합에서 초기화하는 생성자 템플릿이 튜플로부터 생성하는 생성자 템플릿보다 더 잘 일치한다는 점이다. 이 문제를 해결하려면 tail의 길이가 예상한 것보다 다를 때, std::enable_if<>를 사용하여 위의 두 생성자 템플릿을 비활성화시키면 된다.
template<typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail) == sizeof...(Tail)>>
Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
template<typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail) == sizeof...(Tail)>>
Tuple(Tuple<VHead, VTail...> const& other) : head(other.getHead()), tail(other.getTail()) {}
또한, makeTuple() 함수 템플릿으로 튜플을 조금 더 쉽게 생성할 수 있도록 할 수 있다. 이 함수 템플릿은 Tuple의 요소 타입을 결정하기 위해 연역(deduction)을 사용한다.
template<typename... Types>
auto makeTuple(Types&&... elems) {
return Tuple<std::decay_t<Types>...>(std::forward<Types>(elems)...);
}
위 구현에서는 perfect forwarding과 std::decacy_t<> 특질을 결합하여 문자열 리터럴과 raw array를 포인터로 변환하고 const와 reference를 제거한다. 예를 들어, 아래 코드는
makeTuple(17, 3.14, "Hello, World!");
아래 튜플로 초기화된다.
Tuple<int, double, char const*>
지금까지의 구현 코드는 다음과 같다.
template<typename... Types>
class Tuple;
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
{
private:
Head head;
Tuple<Tail...> tail;
public:
Tuple() {}
Tuple(Head const& head, Tuple<Tail...> const& tail) : head(head), tail(tail) {}
Tuple(Head const& head, Tail const&... tail) : head(head), tail(tail) {}
template<typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail) == sizeof...(Tail)>>
Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
template<typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail) == sizeof...(Tail)>>
Tuple(Tuple<VHead, VTail...> const& other) : head(other.getHead()), tail(other.getTail()) {}
Head& getHead() { return head; }
Head const& getHead() const { return head; }
Tuple<Tail...>& getTail() { return tail; }
Tuple<Tail...> const& getTail() const { return tail; }
};
template<>
class Tuple<> {}; // no storage for empty tuple
// get method
template<unsigned N>
struct TupleGet {
template<typename Head, typename... Tail>
static auto const& apply(Tuple<Head, Tail...> const& t) {
return TupleGet<N-1>::apply(t.getTail());
}
};
template<>
struct TupleGet<0> {
template<typename Head, typename... Tail>
static Head const& apply(Tuple<Head, Tail...> const& t) {
return t.getHead();
}
};
template<unsigned N, typename... Types>
auto const& get(Tuple<Types...> const& t) {
return TupleGet<N>::apply(t);
}
// makeTuple method
template<typename... Types>
auto makeTuple(Types&&... elems) {
return Tuple<std::decay_t<Types>...>(std::forward<Types>(elems)...);
}
Basic Tuple Operations
Comparison
튜플을 비교할 때는 요소들만 비교해주면 된다. 요소 별로 두 정의를 비교하는 operator== 구현은 다음과 같다.
// basic case
bool operator==(Tuple<> const&, Tuple<> const&) {
// empty tuples are always equivalent
return true;
}
// recursive case
template<typename Head1, typename... Tail1,
typename Head2, typename... Tail2,
typename = std::enable_if_t<sizeof...(Tail1) == sizeof...(Tail2)>>
bool operator==(Tuple<Head1, Tail1...> const& lhs, Tuple<Head2, Tail2...> const& rhs) {
return lhs.getHead() == rhs.getHead() && lhs.getTail() == rhs.getTail();
}
튜플 비교는 head 요소에서부터 시작하여 basic case에 도달할 때까지 재귀적으로 tail을 방문하여 각 요소를 비교한다. !=, <, >, <=, >= 연산자도 위와 유사하게 구현할 수 있다.
Output
프로그램 내에서 생성한 튜플 타입을 볼 수 있으면 편리하다. 출력할 수 있는 요소 타입을 가지는 모든 튜플을 출력할 수 있는 operator<<는 아래와 같이 구현할 수 있다.
void printTuple(std::ostream& os, Tuple<> const&, bool isFirst = true) {
os << (isFirst ? '(' : ')');
}
template<typename Head, typename... Tail>
void printTuple(std::ostream& os, Tuple<Head, Tail...> const& t, bool isFirst = true) {
os << (isFirst ? "(" : ", ");
os << t.getHead();
printTuple(strm, t.getTail(), false);
}
template<typename... Types>
std::ostream& operator<<(std::ostream& os, Tuple<Types...> const& t) {
printTuple(os, t);
return os;
}
예를 들어, 아래 코드는
std::cout << makeTuple(1, 2.5, std::string("hello")) << std::endl;
(1, 2.5, hello)를 출력한다.
Tuple Algorithms
튜플 알고리즘은 컴파일 시간과 실행 시간 연산이 모두 필요하다는 점에서 특별하다. 이전 포스팅에서 살펴본 타입리스트 알고리즘과 같이 튜플에 알고리즘을 적용하면 완전 다른 타입의 튜플을 만들 수 있는데, 이런 경우에는 컴파일 시간 연산이 필요하다. 예를 들어, Tuple<int, double, string>의 타입 순서를 뒤집으면 Tuple<string, double, int>가 생성된다. 하지만 동종(homogeneous) 컨테이너에 적용되는 알고리즘(std::vector에 대한 std::reverse() 등)과 같은 튜플 알고리즘은 실행 시간의 코드가 필요하다. 따라서 생성되는 코드의 효율성에 대해서도 주의해야 한다.
Tuples as Typelists
Tuple 템플릿의 실행 시간 컴파일러를 무시하면 이전 포스팅에서 다루었던 Typelist와 정확히 동일한 구조를 갖는다. 즉, 템플릿 타입 파라미터를 얼마든지 받을 수 있는데, 몇 가지 부분 특수화만 갖추면 Tuple을 Typelist로 정확히 바꿀 수 있다.
// determine whether the tuple is empty
template<>
struct IsEmpty<Tuple<>> {
static constexpr bool value = true;
};
// extract front element
template<typename Head, typename... Tail>
class FrontT<Tuple<Head, Tail...>> {
public:
using Type = Head;
};
// remove front element
template<typename Head, typename... Tail>
class PopFrontT<Tuple<Head, Tail...>> {
public:
using Type = Tuple<Tail...>;
};
// add element to the front
template<typename... Types, typename Element>
class PushFrontT<Tuple<Types...>, Element> {
public:
using Type = Tuple<Element, Types...>;
};
// add element to the back
template<typename... Types, typename Element>
class PushBackT<Tuple<Types...>, Element> {
public:
using Type = Tuple<Types..., Element>;
};
위 구현만 있다면 이전 포스팅에서 구현한 모든 타입리스트 알고리즘을 Tuple에 적용할 수 있다. 타입리스트 알고리즘은 아래 코드에서 확인할 수 있다.
https://github.com/junstar92/practice-cpp/blob/main/templates/typelist/typelist.h
예를 들어, 아래 코드는
Tuple<int, double, std::string> t1(17, 3.14, "Hello, World!");
using T2 = PopFront<PushBack<decltype(t1), bool>>;
T2 t2(get<1>(t1), get<2>(t1), true);
std::cout << t2 << std::endl;
(3.14, Hello, World!, 1) 을 출력한다.
Adding to and Removing from a Tuple
튜플의 시작이나 끝에 값과 함께 요소를 추가하는 기능은 튜플 알고리즘의 기본이 된다. 튜플 맨 처음에 요소를 추가하는 pushFront 구현은 다음과 같다.
template<typename... Types, typename V>
PushFront<Tuple<Types...>, V>
pushFront(Tuple<Types...> const& tuple, V const& value) {
return PushFront<Tuple<Types...>, V>(value, tuple);
}
새로운 요소(value)를 이미 존재하는 튜플에 추가하려면 value가 head가 되고 원래 존재하는 튜플은 tail이 되는 새로운 튜플을 생성해야 한다. 결과로 얻는 튜플 타입은 Tuple<V, Types...>이다. 위 구현에서는 튜플 알고리즘이 컴파일 시간과 실행 시간 특성이 얼마나 밀접하게 연관되어 있는지 보여주기 위해서 타입리스트 알고리즘인 PushFront를 사용했다.
이미 존재하는 튜플의 끝에 새로운 요소를 추가하는 것은 조금 더 복잡한데, 튜플을 재귀적으로 탐색하면서 수정된 튜플을 함께 전달해야 하기 때문이다. 튜플 끝에 새로운 요소를 추가하는 pushBack 구현은 다음과 같다.
// basic case
template<typename V>
Tuple<V> pushBack(Tuple<> const&, V const& value) {
return Tuple<V>(value);
}
// recursive case
template<typename Head, typename... Tail, typename V>
Tuple<Head, Tail..., V>
pushBack(Tuple<Head, Tail...> const& tuple, V const& value) {
return Tuple<Head, Tail..., V>(tuple.getHead(), pushBack(tuple.getTail(), value));
}
튜플의 맨 처음 요소를 제거한 튜플을 얻을 수 있는 popFront 구현은 다음과 같다.
template<typename... Types>
PopFront<Tuple<Types...>>
popFront(Tuple<Types...> const& tuple) {
return tuple.getTail();
}
Reversing a Tuple
튜플 요소 순서를 역전하는 알고리즘은 타입리스트의 역전 알고리즘과 같은 방식으로 구현한다.
// this implementation is not optimized. it makes multiple copies of elements.
// basic case
Tuple<> reverse(Tuple<> const& t) {
return t;
}
// recursive case
template<typename Head, typename... Tail>
Reverse<Tuple<Head, Tail...>> reverse(Tuple<Head, Tail...> const& t) {
return pushBack(reverse(t.getTail()), t.getHead());
}
Basic case에서는 크게 다를 게 없지만, recursive case에서는 리스트의 tail을 역전시킨 다음에 현재 tail을 역전시킨 리스트의 끝에 덧붙인다. 다만, 이 방식이 최적화된 방식은 아닌데, 이에 대한 내용은 바로 다음 섹션에서 알아보자.
타입리스트에서 했던 방식을 그대로 역전시킨 리스트에 대해 popFront()를 호출하면 쉽게 popBack()을 구현할 수 있다.
template<typename... Types>
PopBack<Tuple<Types...>>
popBack(Tuple<Types...> const& tuple) {
return reverse(popFront(reverse(tuple)));
}
Index Lists
방금 전 구현한 reverse에서 사용한 재귀적 방식 자체가 잘못된 것은 아니지만 실행 시간에 불필요하게 비효율적이다. 이 문제가 얼마나 비효율적인지 확인하기 위해서 객체 자신이 복사되는 횟수를 카운트하는 간단한 클래스로 시작해보자.
template<int N>
struct CopyCounter
{
inline static unsigned numCopies = 0;
CopyCounter() {}
CopyCounter(CopyCounter const&) {
++numCopies;
}
};
그리고 CopyCounter 인스턴스로 구성된 튜플을 생성하고 이 튜플을 역전시켜보자.
void test_copycounter() {
Tuple<CopyCounter<0>, CopyCounter<1>, CopyCounter<2>, CopyCounter<3>, CopyCounter<4>> copies;
auto reversed = reverse(copies);
std::cout << "0: " << CopyCounter<0>::numCopies << " copies\n";
std::cout << "1: " << CopyCounter<1>::numCopies << " copies\n";
std::cout << "2: " << CopyCounter<2>::numCopies << " copies\n";
std::cout << "3: " << CopyCounter<3>::numCopies << " copies\n";
std::cout << "4: " << CopyCounter<4>::numCopies << " copies\n";
}
위 코드의 출력은 다음과 같다.
0: 5 copies
1: 8 copies
2: 9 copies
3: 8 copies
4: 5 copies
단순히 역전시키는 것 뿐인데 복사가 매우 많이 발생한다는 것을 확인할 수 있다. 이상적인 구현은 원래 튜플에서 결과 튜플의 정확한 위치로 딱 한 번의 복사만 수행되어야 한다. Reference를 잘 사용하면 이를 달성할 수 있지만 대신 구현이 굉장히 복잡해진다.
이러한 불필요한 복사를 없애기 위해서 길이를 알고 있는 한 튜플(요소가 5개)의 역전 알고리즘을 makeTuple()과 get()을 사용하여 간단히 구현할 수 있다.
auto reversed = makeTuple(get<4>(copies), get<3>(copies), get<2>(copies),
get<1>(copies), get<0>(copies));
이렇게 생성된 튜플은 단 한 번만 각 요소를 복사한다.
0: 1 copies
1: 1 copies
2: 1 copies
3: 1 copies
4: 1 copies
Index Lists(Index Sequence라고도 부른다)는 이런 아이디어를 일반화한 것이며, 튜플의 인덱스 집합(위의 경우 4, 3, 2, 1, 0)을 parameter pack으로 갖는다. 그러면 pack expansion을 통해 get 호출의 시퀀스를 생성할 수 있게 된다. 표준 라이브러리에서는 인덱스 리스트를 std::integer_sequence(from C++14)로 제공한다.
Reversal with Index Lists
인덱스 리스트를 사용하여 튜플을 역전시키려면 먼저 인덱스 리스트를 표현해야 한다. 인덱스 리스트는 타입리스트나 이종 데이터 구조에서 인덱스로 사용될 값들을 갖는 타입리스트이다. 여기서는 이전 포스팅에서 다루었던 Valuelist 타입을 사용한다. 튜플을 역전시키는 예제 코드에 해당하는 인덱스 리스트는 다음과 같다.
Valuelist<unsigned, 4, 3, 2, 1, 0>
이러한 인덱스 리스트는 간단한 템플릿 메타프로그램 MakeIndexList를 사용하여 0부터 N-1까지 세어 올라가도록 만들 수 있다.
// recursive case
template<unsigned N, typename Result = Valuelist<unsigned>>
struct MakeIndexListT
: MakeIndexListT<N-1, PushFront<Result, CTValue<unsigned, N-1>>> {};
// basic case
template<typename Result>
struct MakeIndexListT<0, Result> {
using Type = Result;
};
template<unsigned N>
using MakeIndexList = typename MakeIndexListT<N>::Type;
위의 연산과 타입리스트 알고리즘 Reverse를 결합하여 튜플 역전을 위한 인덱스 리스트를 만들 수 있다.
using MyIndexList = Reverse<MakeIndexList<5>>;
// equivalent to Valuelist<unsigned, 4, 3, 2, 1, 0>
실제로 튜플을 역전시키려면 인덱스 리스트에 있는 인덱스가 타입이 아닌 parameter pack에 들어 있어야 한다. 이는 인덱스 집합 튜플 reverse() 알고리즘 구현을 둘로 쪼개면 처리할 수 있다.
template<typename... Elements, unsigned... Indices>
auto reverseImpl(Tuple<Elements...> const& t, Valuelist<unsigned, Indices...>) {
return makeTuple(get<Indices>(t)...);
}
template<typename... Elements>
auto reverse(Tuple<Elements...> const& t) {
return reverseImpl(t, Reverse<MakeIndexList<sizeof...(Elements)>>());
}
reverseImpl() 함수 템플릿은 Valuelist 파라미터에서 인덱스를 알아내 parameter pack인 Indices에 보관한다. 그런 다음 보관한 인덱스의 집합으로 튜플에서 get()을 호출하여 요소 인자들을 만들고, 그 인자들로 makeTuple()을 호출하여 그 결과를 리턴한다.
reverse() 알고리즘 자체는 그저 적절한 인덱스 집합을 생성할 뿐이다. 생성된 인덱스 집합은 reverseImpl 알고리즘에 제공된다. 인덱스는 템플릿 메타프로그램으로 조작하므로 실행 시간 코드는 없다. 실행 시간 코드는 reverseImpl에 있는데, 첫 번째 단계에서 결과 튜플을 생성하기 위해 생성하는 makeTuple()을 사용하는 코드가 이에 해당한다. 이때 튜플 요소들이 한 번씩 복사된다.
Shuffle and Select
앞서 구현한 reverseImpl() 함수 템플릿은 사실 reverse() 연산만을 위한 코드를 사용하지는 않았다. 그보단 이미 존재하는 튜플로부터 특정 인덱스 집합을 선택하여 새로운 튜플을 만들었을 뿐이다. reverse()는 순서를 역전시킨 인덱스 집합을 제공할 뿐이지만, 다른 많은 알고리즘은 select() 알고리즘을 사용하여 구현할 수 있다.
template<typename... Elements, unsigned... Indices>
auto select(Tuple<Elements...> const& t, Valuelist<unsigned, Indices...>) {
return makeTuple(get<Indices>(t)...);
}
select()를 사용하여 구현할 수 있는 간단한 알고리즘으로는 튜플의 splat 연산이 있다. 이 연산은 튜플에서 한 요소를 선택하여 복사한 후, 그 요소를 여러 개 가지는 새로운 튜플을 만든다.
template<unsigned I, unsigned N, typename IndexList = Valuelist<unsigned>>
class ReplicatedIndexListT;
template<unsigned I, unsigned N, unsigned... Indices>
class ReplicatedIndexListT<I, N, Valuelist<unsigned, Indices...>>
: public ReplicatedIndexListT<I, N-1, Valuelist<unsigned, Indices..., I>> {};
template<unsigned I, unsigned... Indices>
class ReplicatedIndexListT<I, 0, Valuelist<unsigned, Indices...>> {
public:
using Type = Valuelist<unsigned, Indices...>;
};
template<unsigned I, unsigned N>
using ReplicatedIndexList = typename ReplicatedIndexListT<I, N>::Type;
template<unsigned I, unsigned N, typename... Elements>
auto splat(Tuple<Elements...> const& t) {
return select(t, ReplicatedIndexList<I, N>());
}
이 코드는 다음과 같이 사용할 수 있다.
Tuple<int, double, std::string> t(42, 7.7, "hello");
auto a = splat<1, 4>(t);
std::cout << a << std::endl;
위 코드는 Tuple<double, double, double, double>을 만들고, 각 값은 get<1>(t)의 복사본이므로 (7.7, 7.7, 7.7, 7.7)을 출력한다.
더욱 복잡한 튜플 알고리즘이라도 select()를 활용하면 인덱스 리스트에 대한 템플릿 메타프로그램으로 구현할 수 있다. 예를 들어, 요소 타입의 크기에 따라 튜플을 정렬하기 위해서 타입리스트에서 구현한 삽입 정렬을 사용할 수 있다. 비교 연산으로 튜플 요소 타입을 비교할 수 있는 템플릿 메타함수를 받는 sort() 함수가 있다면 아래와 같은 코드로 튜플 요소를 크기에 따라 정렬할 수 있다.
// matafunction wrapper that compares the elements in a tuple
template<typename List, template<typename T, typename U> class F>
class MetafunOfNthElementT {
public:
template<typename T, typename U> class Apply;
template<unsigned N, unsigned M>
class Apply<CTValue<unsigned, M>, CTValue<unsigned, N>>
: public F<NthElement<List, M>, NthElement<List,N>> {};
};
// sort a tuple based on comparing the element types
template<template<typename T, typename U> class Compare, typename... Elements>
auto sort(Tuple<Elements...> const& t) {
return select(t, InsertionSort<MakeIndexList<sizeof...(Elements)>,
MetafunOfNthElementT<Tuple<Elements...>, Compare>::template Apply>());
}
이렇게 구현한 sort()는 다음과 같이 사용할 수 있다.
template<typename T, typename U>
class SamllerThanT {
public:
static constexpr bool value = sizeof(T) < sizeof(U);
};
void test_tuplesort() {
auto t1 = makeTuple(17LL, std::complex<double>(42, 77), 'c', 42, 7.7);
std::cout << t1 << std::endl;
auto t2 = sort<SamllerThanT>(t1);
std::cout << "sorted by size: " << t2 << std::endl;
}
test_tuplesort() 출력은 다음과 같다.
(17, (42,77), c, 42, 7.7)
sorted by size: (c, 42, 7.7, 17, (42,77))
Expanding Tuples
투플은 서로 관련된 값들을 하나의 값으로 저장할 때 유용하다. 그 값들의 타입이 무엇이든지 상관없고, 값이 몇 개이든지도 상관없다. 하지만 어느 순간에 튜플의 값들을 unpack하는 것이 필요하다. 예를 들어, 튜플의 요소 몇 개를 함수의 파라미터로 전달해야 하는 순간이 있다. 튜플을 전달받아서 각 튜플 요소를 출력하는 함수 print가 있다고 생각해보자.
Tuple<std::string, char const*, int, char> t("Pi", "is roughly", 3, '\n');
print(t...); // ERROR: cannot expand a tuple; it isn't a parameter pack
위 코드에서 말하는 바와 같이 튜플은 parameter pack이 아니기 때문에 이러한 방식으로 unpack하는 것은 불가능하다. 튜플을 unpack하려면 인덱스 리스트를 사용해야 한다. 아래 함수 템플릿 applyFn()는 튜플을 받아서 튜플을 요소 별로 unpack하여 함수를 호출한다.
template<typename F, typename... Elements, unsigned... Indices>
auto applyImpl(F f, Tuple<Elements...> const& t, Valuelist<unsigned, Indices...>) -> decltype(f(get<Indices>(t)...))
{
return f(get<Indices>(t)...);
}
template<typename F, typename... Elements, unsigned N = sizeof...(Elements)>
auto applyFn(F f, Tuple<Elements...> const& t) -> decltype(applyImpl(f, t, MakeIndexList<N>())) {
return applyImpl(f, t, MakeIndexList<N>());
}
applyImpl() 함수 템플릿은 인덱스 리스트를 인자로 받아서 튜플의 각 요소를 unpack하여 f를 위한 인자 리스트를 생성한다. applyFn을 아래와 같이 사용하면 우리가 원하는 동작을 얻을 수 있다.
Tuple<std::string, char const*, int, char> t7("Pi", "is roughly", 3, '\n');
applyFn(print<std::string, char const*, int, char>, t7);
여기서 사용된 print 함수는 다음과 같다.
template<typename T, typename... Types>
void print(T const& firstArg, Types const&... args) {
std::cout << firstArg << " ";
if constexpr(sizeof...(args) > 0) {
print(args...);
}
}
여기서 print 함수 객체를 전달할 때, print의 템플릿 파라미터 리스트를 명시적으로 나타내고 있다. 템플릿 파라미터 리스트를 명시적으로 나타내지 않으면 'couldn't infer template argument' 라는 컴파일 에러가 발생한다. 이는 전달하는 함수 객체가 템플릿이기 때문에 발생하는 것으로 확인되며, 람다나 다른 방식을 통해 함수 이름만으로도 호출이 가능할 것으로 보인다.
위의 applyFn은 표준 라이브러리에서 std::apply 함수 템플릿으로 제공하고 있다.
지금까지의 구현 코드는 아래와 같다. 여기서 사용되는 typelist.h와 valuelist.h는 링크를 통해 확인할 수 있다.
#pragma once
#include <iosfwd>
#include <utility>
#include "../typelist/typelist.h"
#include "../typelist/valuelist.h"
template<typename... Types>
class Tuple;
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
{
private:
Head head;
Tuple<Tail...> tail;
public:
Tuple() {}
Tuple(Head const& head, Tuple<Tail...> const& tail) : head(head), tail(tail) {}
Tuple(Head const& head, Tail const&... tail) : head(head), tail(tail...) {}
template<typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail) == sizeof...(Tail)>>
Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
template<typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail) == sizeof...(Tail)>>
Tuple(Tuple<VHead, VTail...> const& other) : head(other.getHead()), tail(other.getTail()) {}
Head& getHead() { return head; }
Head const& getHead() const { return head; }
Tuple<Tail...>& getTail() { return tail; }
Tuple<Tail...> const& getTail() const { return tail; }
};
template<>
class Tuple<> {}; // no storage for empty tuple
// get method
template<unsigned N>
struct TupleGet {
template<typename Head, typename... Tail>
static auto const& apply(Tuple<Head, Tail...> const& t) {
return TupleGet<N-1>::apply(t.getTail());
}
};
template<>
struct TupleGet<0> {
template<typename Head, typename... Tail>
static Head const& apply(Tuple<Head, Tail...> const& t) {
return t.getHead();
}
};
template<unsigned N, typename... Types>
auto const& get(Tuple<Types...> const& t) {
return TupleGet<N>::apply(t);
}
// makeTuple method
template<typename... Types>
auto makeTuple(Types&&... elems) {
return Tuple<std::decay_t<Types>...>(std::forward<Types>(elems)...);
}
// comparison
bool operator==(Tuple<> const&, Tuple<> const&) {
// empty tuples are always equivalent
return true;
}
template<typename Head1, typename... Tail1,
typename Head2, typename... Tail2,
typename = std::enable_if_t<sizeof...(Tail1) == sizeof...(Tail2)>>
bool operator==(Tuple<Head1, Tail1...> const& lhs, Tuple<Head2, Tail2...> const& rhs) {
return lhs.getHead() == rhs.getHead() && lhs.getTail() == rhs.getTail();
}
// Output
void printTuple(std::ostream& strm, Tuple<> const&, bool isFirst = true) {
strm << (isFirst ? '(' : ')');
}
template<typename Head, typename... Tail>
void printTuple(std::ostream& strm, Tuple<Head, Tail...> const& t, bool isFirst = true) {
strm << (isFirst ? "(" : ", ");
strm << t.getHead();
printTuple(strm, t.getTail(), false);
}
template<typename... Types>
std::ostream& operator<<(std::ostream& strm, Tuple<Types...> const& t) {
printTuple(strm, t);
return strm;
}
/* Tuple as Typelists */
// determine whether the tuple is empty
template<>
struct IsEmpty<Tuple<>> {
static constexpr bool value = true;
};
// extract front element
template<typename Head, typename... Tail>
class FrontT<Tuple<Head, Tail...>> {
public:
using Type = Head;
};
// remove front element
template<typename Head, typename... Tail>
class PopFrontT<Tuple<Head, Tail...>> {
public:
using Type = Tuple<Tail...>;
};
// add element to the front
template<typename... Types, typename Element>
class PushFrontT<Tuple<Types...>, Element> {
public:
using Type = Tuple<Element, Types...>;
};
// add element to the back
template<typename... Types, typename Element>
class PushBackT<Tuple<Types...>, Element> {
public:
using Type = Tuple<Types..., Element>;
};
// pushFront
template<typename... Types, typename V>
PushFront<Tuple<Types...>, V>
pushFront(Tuple<Types...> const& tuple, V const& value) {
return PushFront<Tuple<Types...>, V>(value, tuple);
}
// pushBack
template<typename V>
Tuple<V> pushBack(Tuple<> const&, V const& value) {
return Tuple<V>(value);
}
template<typename Head, typename... Tail, typename V>
Tuple<Head, Tail..., V>
pushBack(Tuple<Head, Tail...> const& tuple, V const& value) {
return Tuple<Head, Tail..., V>(tuple.getHead(), pushBack(tuple.getTail(), value));
}
// popFront
template<typename... Types>
PopFront<Tuple<Types...>>
popFront(Tuple<Types...> const& tuple) {
return tuple.getTail();
}
// reverse
// this implementation is not optimized. it makes multiple copies of elements.
Tuple<> reverseUnopt(Tuple<> const& t) {
return t;
}
template<typename Head, typename... Tail>
Reverse<Tuple<Head, Tail...>> reverseUnopt(Tuple<Head, Tail...> const& t) {
return pushBack(reverseUnopt(t.getTail()), t.getHead());
}
// popBack
template<typename... Types>
PopBack<Tuple<Types...>>
popBack(Tuple<Types...> const& tuple) {
return reverse(popFront(reverse(tuple)));
}
// reversal with index lists
template<unsigned N, typename Result = Valuelist<unsigned>>
struct MakeIndexListT
: MakeIndexListT<N-1, PushFront<Result, CTValue<unsigned, N-1>>> {};
template<typename Result>
struct MakeIndexListT<0, Result> {
using Type = Result;
};
template<unsigned N>
using MakeIndexList = typename MakeIndexListT<N>::Type;
template<typename... Elements, unsigned... Indices>
auto reverseImpl(Tuple<Elements...> const& t, Valuelist<unsigned, Indices...>) {
return makeTuple(get<Indices>(t)...);
}
template<typename... Elements>
auto reverse(Tuple<Elements...> const& t) {
return reverseImpl(t, Reverse<MakeIndexList<sizeof...(Elements)>>());
}
// shuffle and select
template<typename... Elements, unsigned... Indices>
auto select(Tuple<Elements...> const& t, Valuelist<unsigned, Indices...>) {
return makeTuple(get<Indices>(t)...);
}
template<unsigned I, unsigned N, typename IndexList = Valuelist<unsigned>>
class ReplicatedIndexListT;
template<unsigned I, unsigned N, unsigned... Indices>
class ReplicatedIndexListT<I, N, Valuelist<unsigned, Indices...>>
: public ReplicatedIndexListT<I, N-1, Valuelist<unsigned, Indices..., I>> {};
template<unsigned I, unsigned... Indices>
class ReplicatedIndexListT<I, 0, Valuelist<unsigned, Indices...>> {
public:
using Type = Valuelist<unsigned, Indices...>;
};
template<unsigned I, unsigned N>
using ReplicatedIndexList = typename ReplicatedIndexListT<I, N>::Type;
template<unsigned I, unsigned N, typename... Elements>
auto splat(Tuple<Elements...> const& t) {
return select(t, ReplicatedIndexList<I, N>());
}
// sort
// matafunction wrapper that compares the elements in a tuple
template<typename List, template<typename T, typename U> class F>
class MetafunOfNthElementT {
public:
template<typename T, typename U> class Apply;
template<unsigned N, unsigned M>
class Apply<CTValue<unsigned, M>, CTValue<unsigned, N>>
: public F<NthElement<List, M>, NthElement<List,N>> {};
};
// sort a tuple based on comparing the element types
template<template<typename T, typename U> class Compare, typename... Elements>
auto sort(Tuple<Elements...> const& t) {
return select(t, InsertionSort<MakeIndexList<sizeof...(Elements)>,
MetafunOfNthElementT<Tuple<Elements...>, Compare>::template Apply>());
}
// expanding tuples
template<typename F, typename... Elements, unsigned... Indices>
auto applyImpl(F f, Tuple<Elements...> const& t, Valuelist<unsigned, Indices...>) -> decltype(f(get<Indices>(t)...))
{
return f(get<Indices>(t)...);
}
template<typename F, typename... Elements, unsigned N = sizeof...(Elements)>
auto applyFn(F f, Tuple<Elements...> const& t) -> decltype(applyImpl(f, t, MakeIndexList<N>())) {
return applyImpl(f, t, MakeIndexList<N>());
}
Optimizing Tuple
이번에는 Tuple 구현에 적용할 수 있는 몇 가지 최적화 방안에 대해서 살펴본다.
Tuples and the EBCO
위의 구현대로 Tuple을 사용한다면 실제로 필요한 것보다 더 많은 공간을 차지한다. 한 가지 문제는 멤버 변수 tail이 결국에는 empty라는 것인데, 모든 nonempty tuple은 결국 empty tuple로 끝나고 데이터 멤버는 적어도 한 바이트의 공간을 차지한다.
Tuple의 공간 효율성을 향상시키려면 Empty Base Class Optimization(EBCO)를 적용하여 tail을 멤버가 아니라 tail tuple로부터 상속받도록 해야 한다. 아래 코드를 살펴보자.
// recursive case
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...>
{
private:
Head head;
public:
Head& getHead() { return head; }
Head const& getHead() const { return head; }
Tuple<Tail...>& getTail() { return *this; }
Tuple<Tail...> const& getTail() const { return *this; }
};
tail에 해당하는 부분을 상속으로 처리하고 있다. 아쉽지만 이렇게 하면 튜플 요소가 생성자에서 초기화될 때 순서가 뒤바뀌게 된다는 부작용이 있다. 이전 구현에서는 head 멤버가 tail 멤버보다 앞에 있기 때문에 head가 먼저 초기화된다. 하지만 위와 같이 구현하면 tail이 베이스 클래스이므로 멤버인 head보다 먼저 초기화된다.
이 문제는 베이스 클래스 리스트 내에 있는 Tail보다 앞에 있는 베이스 클래스를 만들어 그 클래스 내에 head 멤버를 두면 해결할 수 있다. 이를 직접 구현하려면 각 요소 타입을 둘러싸는 TupleElt 클래스를 도입하여 이를 Tuple이 상속받도록 해도 된다.
template<typename... Types>
class Tuple;
template<typename T>
class TupleElt
{
T value;
public:
TupleElt() = default;
template<typename U>
TupleElt(U&& other) : value(std::forward<U>(other)) {}
T& get() { return value; }
T const& get() const { return value; }
};
// recursive case
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
: private TupleElt<Head>, private Tuple<Tail...>
{
public:
Head& getHead() { return head; } // potentially ambigous
Head const& getHead() const { return head; } // potentially ambigous
Tuple<Tail...>& getTail() { return *this; }
Tuple<Tail...> const& getTail() const { return *this; }
};
// basis case
template<>
class Tuple<> {}; // no storage for empty tuple
이 방식은 초기화 순서 문제를 해결할 순 있지만, 더 안 좋은 문제가 발생한다. Tuple<int, int>처럼 동일한 타입의 요소가 두 개면 요소를 추출할 수가 없다. 이는 Tuple에서 해당 타입(이를테면 TupleElt<int>)의 TupleElt로의 파생 클래스로부터 베이스 클래스로의 변환이 모호해지기 때문이다.
이러한 모호함을 해결하려면 각 TupleElt 베이스 클래스는 주어진 Tuple 내에서 고유해야만 한다. Tuple 내에서는 이 값의 'Height', 즉, 마지막 Tuple로부터의 길이를 추가해주는 것이 해결책이 될 수 있다. Tuple 내에서 마지막 요소의 높이는 0이고, 끝에서 바로 앞 요소는 1로 저장되는 등의 방식이다.
template<unsigned Height, typename T>
class TupleElt
{
T value;
public:
TupleElt() = default;
template<typename U>
TupleElt(U&& other) : value(std::forward<U>(other)) {}
T& get() { return value; }
T const& get() const { return value; }
};
이렇게 구현하면 EBCO가 적용된 Tuple을 만들면서 초기화 순서도 유지할 수 있으며, 요소들이 모두 같은 타입이어도 문제없다.
그리고 Tuple을 아래와 같이 구현하고,
template<typename... Types>
class Tuple;
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
: private TupleElt<sizeof...(Tail), Head>, private Tuple<Tail...>
{
using HeadElt = TupleElt<sizeof...(Tail), Head>;
public:
Tuple(Head const& head, Tuple<Tail...> const& tail) : HeadElt(head), Tuple<Tail...>(tail) {}
Tuple(Head const& head, Tail const&... tail) : HeadElt(head), Tuple<Tail...>(tail...) {}
Head& getHead() { return static_cast<HeadElt*>(this)->get(); }
Head const& getHead() const { return static_cast<HeadElt const*>(this)->get(); }
Tuple<Tail...>& getTail() { return *this; }
Tuple<Tail...> const& getTail() const { return *this; }
};
template<>
class Tuple<> {}; // no storage for empty tuple
아래 코드를 실행해보자.
struct A {
A() {
std::cout << "A()\n";
}
};
struct B {
B() {
std::cout << "B()\n";
}
};
int main()
{
Tuple<A, char, A, char, B> t1;
std::cout << sizeof(t1) << " bytes\n";
return 0;
}
위 코드 출력은 다음과 같다.
A()
A()
B()
5 bytes
이전에 구현한 Tuple을 사용하여 테스트해보면 차지하는 공간이 6 bytes라는 것을 알 수 있다.
EBCO를 사용하면 Tuple<>이라는 empty tuple에 대한 1 byte를 제거할 수 있다. 그러나 A와 B가 빈 클래스라면 Tuple 내의 EBCO를 더 적용할 여지가 있다. TupleElt는 그렇게 해도 안전할 때 element type을 상속받도록 더 확장할 수 있다 (Tuple 구현은 건드리지 않고).
template<unsigned Height, typename T,
bool = std::is_class_v<T> && !std::is_final_v<T>>
class TupleElt;
template<unsigned Height, typename T>
class TupleElt<Height, T, false>
{
T value;
public:
TupleElt() = default;
template<typename U>
TupleElt(U&& other) : value(std::forward<U>(other)) {}
T& get() { return value; }
T const& get() const { return value; }
};
template<unsigned Height, typename T>
class TupleElt<Height, T, true> : private T
{
public:
TupleElt() = default;
template<typename U>
TupleElt(U&& other) : T(std::forward<U>(other)) {}
T& get() { return *this; }
T const& get() const { return *this; }
};
이렇게 변경하고 다시 컴파일한 뒤, 실행해보면 아래와 같이 출력하는 것을 확인할 수 있다.
A()
A()
B()
2 bytes
Constant-time get()
튜플에서 get() 함수는 아주 많이 사용된다. 하지만 get()을 재귀로 구현하면 필요한 템플릿 인스턴스화의 수가 선형적으로 증가하게 되어, 컴파일 시간에 영향을 미치게 된다. 다행히 EBCO 최적화를 활용하면 get()도 조금 더 효율적으로 구현할 수 있다.
핵심은 베이스 클래스 타입의 파라미터를 파생 클래스 타입의 인자에 일치시킬 때, template argument dedecution이 베이스 클래스의 템플릿 인자를 추론한다는 점이다. 즉, 추출하고자 하는 요소의 Height를 계산할 수 있으면 요소를 추출할 때 모든 인덱스를 일일이 재귀적으로 찾아들어가는 대신 Tuple 특수화에서 TupleElt<H, T>로 변환되는 방식을 활용할 수 있다 (T는 추론됨).
template<unsigned H, typename T>
T& getHeight(TupleElt<H, T>& te) {
return te.get();
}
template<unsigned I, typename... Elements>
auto get(Tuple<Elements...>& t) -> decltype(getHeight<sizeof...(Elements) - I - 1>(t)) {
return getHeight<sizeof...(Elements) - I - 1>(t);
}
get<I>(t)는 원하는 요소의 인덱스 I를 받는데, 튜플의 실제 저장 공간은 높이인 H 값에 따르기 때문에 H에서 I를 계산해야 한다. getHeight()를 호출하면 템플릿 인자 연역으로 탐색하며, 높이인 H는 명시적으로 제공되므로 고정된 값이다. 따라서, 타입 T가 추론되면 단 하나의 TupleElt 베이스 클래스만 일치하게 된다.
단, getHeight()는 private base class이므로 Tuple의 friend로 선언되어야 한다.
template<typename Head, typename... Tail>
class Tuple<Head, Tail...>
: private TupleElt<sizeof...(Tail), Head>, private Tuple<Tail...>
{
...
template<unsigned I, typename... Elements>
friend auto get(Tuple<Elements...>& t) -> decltype(getHeight<sizeof...(Elements)-I-1>(t));
...
};
위와 같은 구현 방식을 사용하면 튜플 요소 인덱스를 일치시키는 작업을 컴파일러의 템플릿 인자 연역에 맡겨버릴 수 있고, 따라서 필요한 템플릿 인스턴스화의 수가 linear에서 constant로 줄어든다.
위 구현에서는 이전에 구현한 타입리스트 알고리즘을 그대로 사용할 수 없는 것으로 보이며 다르게 다시 구현해야 한다.
Tuple Subscript
원칙적으로 std::vector에서 operator[]를 정의하듯이 튜플의 요소에 액세스하는 operator[]를 정의할 수 있다. 하지만 벡터와는 달리 튜플 요소는 타입이 서로 다를 수 있다. 따라서, 튜플의 operator[]는 요소의 인덱스에 따라 리턴 타입이 달라지도록 템플릿이어야만 한다. 그리고 그 요소의 타입을 정의할 때 인덱스의 타입을 쓸 수 있도록 각 인덱스가 다른 타입을 가져야 한다.
타입리스트에서 살펴본 CTValue 클래스 템플릿을 사용하면 타입 내에 인덱스를 삽입할 수 있다. Tuple 멤버에 subscript 연산자를 아래와 같이 정의해보자.
template<typename T, T Index>
auto& operator[](CTValue<T, Index>) {
return get<Index>(*this);
}
위 코드는 get<>()을 호출할 때 CTValue 인자의 타입 내에 전달된 인덱스 값을 사용한다. 그러면 다음과 같이 사용할 수 있다.
auto t = makeTuple(0, '1', 2.2f, std::string("hello"));
auto a = t[CTValue<unsigned, 2>{}];
auto b = t[CTValue<unsigned, 3>{}];
a와 b는 Tuple t의 세 번째, 네 번째 값의 타입과 값으로 초기화된다.
상수 인덱스를 조금 더 쉽게 사용하기 위해서 '_c' 접미사로 끝나는 리터럴로부터 컴파일 타입 리터럴을 바로 계산하는 constexpr 리터럴 연산자를 구현할 수도 있다.
#include <cassert>
// convert single char to corresponding int value at compile time
constexpr int toInt(char c) {
// hexadecimal letters
if (c >= 'A' && c <= 'F') {
return static_cast<int>(c) - static_cast<int>('A') + 10;
}
if (c >= 'a' && c <= 'f') {
return static_cast<int>(c) - static_cast<int>('a') + 10;
}
// other (disable '.' for floating-point literals)
assert(c >= '0' && c <= '9');
return static_cast<int>(c) - static_cast<int>('0');
}
// parse array of chars to corresponding int value at compile time
template<std::size_t N>
constexpr int parseInt(char const (&arr)[N]) {
int base = 10; // to handle base (default: decimal)
int offset = 0; // to skip prefixes like 0x
if (N > 2 && arr[0] == '0') {
switch (arr[1]) {
case 'x':
case 'X':
base = 16;
offset = 2;
break;
case 'b':
case 'B':
base = 2;
offset = 2;
break;
default:
base = 8;
offset = 1;
break;
}
}
// iterate over all digits and compute resulting value
int value = 0;
int multiplier = 1;
for (std::size_t i = 0; i < N - offset; ++i) {
if (arr[N-1-i] != '\'') {
value += toInt(arr[N-1-i]) * multiplier;
multiplier *= base;
}
}
return value;
}
// literal operator: parse integral literals with suffix '_c' as sequence of chars
template<char... cs>
constexpr auto operator"" _c() {
return CTValue<int, parseInt<sizeof...(cs)>({cs...})>{};
}
문자는 constexpr 헬퍼 함수는 parseInt()로 전달하여 컴파일 타임에 문자 시퀀스의 값을 계산한 뒤, CTValue를 만든다.
- 42_c -> CTValue<int, 42>
- 0x518_c -> CTValue<int, 2069>
- 0b1111'1111_c -> CTValue<int, 255>
그러면 다음과 같이 operator[]를 사용할 수 있다.
auto t = makeTuple(0, '1', 2.2f, std::string("hello"));
auto a = t[2_c];
auto b = t[3_c];
'프로그래밍 > C & C++' 카테고리의 다른 글
[C++] Argument Parser (python argparse like) (0) | 2024.01.02 |
---|---|
[C++] 템플릿과 상속 (EBCO, CRTP) (0) | 2023.12.29 |
[C++] Typelists (0) | 2023.12.23 |
[C++] 메타프로그래밍 (0) | 2023.12.16 |
[C++] Type Erasure (4) | 2023.12.15 |
댓글