C++에서 템플릿 메타프로그래밍하다가 발생한 오류
이슈
저의 OAGLE에서는 정점 버퍼를 만드는 것을 캡슐화할 때 원래 한 종류의 정점 클래스만 사용하고 있었는데, 그것은 위치(3차원 실수 벡터), 법선(3차원 실수 벡터), 텍스처 좌표(2차원 실수 벡터), 법선을 위한 tangent와 bitangent(각각 3차원 실수 벡터인데, 탄젠트는 접선이란 뜻이지만, 엄밀히 말해 실질적 의미는 법선에 수직한 2개 그래디언트에 가까우므로 따로 번역하지 않음), 뼈 인덱스(4차원 정수 벡터), 대응 뼈 가중치(4차원 실수 벡터)로, 생각해 보면 이것저것 구현함에 있어 꽤나 무겁거나 뻣뻣한 느낌이 있습니다.
그리하여 템플릿 매개변수를 주기만 하면 그에 따라 알아서 VAO를 만들어 주는 클래스를 새로 추가했습니다. 바인드한 정점 데이터는 셰이더 코드에서 그에 맞게 읽으면 되니 이것만 성공적으로 만들면 된다는 말이죠. 만드는 것 자체는 별로 어렵지 않았습니다.
template <class... T>
struct CustomVertex; // 나머지를 포괄하는 원형을 선언
template <class F>
struct CustomVertex{
F first;
template<unsigned P> constexpr auto& get(){
static_assert(P==0, "Index exceeded");
return first;
}
};
template <class F, class... T>
struct CustomVertex{
F first;
CustomVertex<T...> rest;
template<unsigned P> constexpr auto& get(){
static_assert(sizeof...(T) >= P, "Index exceeded");
if constexpr(P==0) return first;
else return rest.get<P-1>();
}
};
/*
이때, CustomVertex<a, b, c> 타입은 struct {a a1; struct{b b1; struct{c c1;}; }; }과 완전히 동일한 구조를 가지게 됨
이는 통상 구조체보다 메모리를 더 많이 차지할 가능성이 있지만,
설령 double 같은 걸 쓴대도 자동이라는 편의를 얻고 메모리를 살짝 내주는 겁니다.
초기화는 초기화 리스트를 통해 바로 할 수 있음
*/
그리고 각 타입 확인은 이것을 이용하여 나름대로 축약할 수 있습니다.
template<class A>
inline constexpr bool isOneOf() {
return false;
}
template <class A, class T1, class... Types>
inline constexpr bool isOneOf() {
return std::is_same_v<A, T1> || isOneOf<A, Types...>();
}
VAO를 만들 때 위 과정을 통해 자동으로 정점 속성 포인터를 설정하는 것은 지금 다루려는 주제가 아니니 생략합니다. 알고자 한다면 여기를 참고하세요. struct CusteomVertex 부분을 ctrl+F로 찾으면 됩니다.
참고로 std::tuple을 쓰는 것은 불가능한 처사는 아니겠지만 MSVC 기준, 메모리 상 순서가 선언한 것의 반대이기 때문에 찝찝한 면이 있습니다.
문제는 말이죠, offsetof() 매크로를 get 템플릿 함수와 함께 사용할 수 없다는 것입니다. IDE 상에서는 알아서 값을 구해 주지만 컴파일러 작동 시 바로 오류가 발생합니다. 이렇게요.
VAO를 활용함에 있어 offsetof 매크로를 따로 호출할 이유는 없고, VAO를 만들 때 필요한 오프셋 자체는 컴파일 타임에 알아서 구하도록 되어 있습니다. get이랑은 같이 못 쓰지만 offsetof(vert, rest.rest.rest.rest) 같은 코드는 전혀 쓰고 싶어 보이지 않지만 역시 컴파일 가능하기도 합니다. 그렇지만 저는 조금 더 어엿한 클래스 같은 인터페이스를 제공하고 싶습니다.
문제: 내 보기에는 문제가 없어 보이는 아래 템플릿 코드를 잘 작동하게 바꾸기
저는 먼저 이런 것을 생각해 보았습니다.
template<unsigned P> inline static constexpr size_t offsetOf() { // 탈출조건: 매개변수 하나짜리 클래스
static_assert(P == 0, "Index exceeded.");
return 0;
}
template<unsigned P> inline static constexpr size_t offsetOf() {
static_assert(sizeof...(T) >= P, "Index exceeded.");
using thisType = CustomVertex < F, T...>;
if constexpr (P == 0)return 0;
return offsetof(thisType, rest) + CustomVertex<T...>::offsetOf<P - 1>();
}
결과는 위의 경우와 비슷한데, 3개의 오류 메시지가 존재합니다.
저 3개 오류를 클릭해 보면, 모두 offsetOf<P-1>(); 괄호 중에 정확히 저 빨간 부분에서 오류가 발생했다고 알려 줍니다. 나머지는 일단 눈으로 보기엔 구문이 안 틀렸는데요, 템플릿 푸는 도중에 뭔가 꼬였나 봅니다. 이건 생각하기 복잡하니 3번째는 봅시다. 같이 있는 jump_statement는 return문뿐입니다만 딱히 제가 리턴문을 끝내지는 않았는데요. 탈출조건에는 당연히 문제가 없으니, 리턴문만 다른 걸로 바꿔 봤습니다.
template<unsigned P> inline static constexpr size_t offsetOf() {
static_assert(sizeof...(T) >= P, "Index exceeded.");
using thisType = CustomVertex < F, T...>;
if constexpr (P == 0)return 0;
size_t under = CustomVertex<T...>::offsetOf<P - 1>();
return offsetof(thisType, rest) + under;
}
문제가 되는 코드를 리턴문보다 앞 문장에서 마쳤습니다. 오류는 2개가 됩니다.
jump_statement가 simple_declaration으로 바뀌고 ;가 예상되었다는 말이 없어졌네요. simple_declaration 쪽에 있는 함수는 하나뿐인 관계로, 하지만 애초에 저 3가지는 같은 걸 가리킨 거라서 수가 줄어도 전혀 상관 없는 것 같습니다.
매개변수로 전달한 경우에는 괜찮습니다.
template<unsigned P>inline static constexpr size_t offsetOf(size_t h) {
static_assert(sizeof...(T) >= P, "index exceeded");
if constexpr (P == 0) return h;
using thisType = CustomVertex < F, T...>;
return CustomVertex<T...>::offsetOf<P - 1>(h + offsetof(thisType, rest));
}
이 코드는 도구 단계에서 오프셋 값을 제대로 보여주기도 하고 컴파일도 잘 됩니다. 하지만 은닉이 필요하다는 문제가 있어요. 매개변수 h는 항상 0을 받아야 맞지 않을까요? 그 외의 값을 주면 실제 오프셋 + h라는 아무런 의미 없는 값을 줄 테니 말입니다. 물론 컴파일 타임에 만드는 걸 의미 없는 값을 받는다는 건 프로그래머의 악의겠지만 조금이라도 무결하기 위해 private로 숨겼습니다. 최종적으로 이렇게 하니까 되는군요.
template<class... T>
struct CustomVertex;
template<class F>
struct CustomVertex<F> {
template<typename...>friend struct CustomVertex;
F first;
operator F() { return first; }
CustomVertex& operator=(const F& a) { return first = a; }
template<unsigned POS>
constexpr auto& get() {
static_assert(POS == 0, "index exceeded");
return first;
}
template<unsigned P>inline static constexpr size_t offsetOf() {
return offsetOf<P>(0);
}
private:
template<unsigned P> inline static constexpr size_t offsetOf(size_t h) {
static_assert(P == 0, "Index exceeded.");
return h;
}
};
template <class F, class... T>
struct CustomVertex<F, T...> {
template<typename...> friend struct CustomVertex;
F first;
CustomVertex<T...> rest;
template<unsigned POS>
constexpr auto& get() {
static_assert(sizeof...(T) >= POS, "index exceeded");
if constexpr (POS == 0) return first;
else return rest.get<POS - 1>();
}
template<unsigned P>inline static constexpr size_t offsetOf() {
return offsetOf<P>(0);
}
private:
template<unsigned P>inline static constexpr size_t offsetOf(size_t h) {
static_assert(sizeof...(T) >= P, "index exceeded");
if constexpr (P == 0) return h;
using thisType = CustomVertex < F, T...>;
return CustomVertex<T...>::offsetOf<P - 1>(h + offsetof(thisType, rest));
}
};
템플릿 메타프로그래밍은 참 어렵습니다.
return CustomVertex<T...>::offsetOf<P - 1>(offsetof(thisType, rest)) + h;
// 이거랑 이게 뭐가 다르길래..?
return offsetof(thisType, rest) + CustomVertex<T...>::offsetOf<P - 1>();
이런 건 어떤 식으로 검색할지도 잘 모르겠네요. 정말정말로 가끔 컴파일러 잘못이기도 한데(IDE 차원에서는 이미 해석 완료됐으니까) 진실은 템플릿을 풀어헤친 기계만 알겠죠. 나중에 알면 추가하겠습니다. (추가) 우연히 https://cppinsights.io/라는 곳을 발견했는데 템플릿을 풀어 주는군요. 잘 활용해 보아야겠습니다.
C++ Insights
C++ Insights - See your source code with the eyes of a compiler.
cppinsights.io
(내용 추가)다중 상속을 활용한 더 컴팩트한 해법을 개발했고 MSVC, Clang, GCC(G++)에서 사용이 가능합니다. 코드는 여기 있고, 이건 시간이 나면 여기에 추가해 보겠습니다.