A Journey to get a self type alias

김가람·2023년 4월 15일
0

최근에 나온 언어들은(예를 들어 rust, swift) 오브젝트 자신에 대한 타입 엘리어스(Self 등) 을 제공한다. 다만 C++는 지원하지않는다. 물론 매크로와 타이핑을 직접 치는 수고로움을 동반한다면 자기 자신의 타입을 가져오는게 가능하지만 귀찮지않은가? 모든 클래스마다 DEFINE_SELF(....)이런걸 치는건 귀찮다. 개인 프로젝트에서 오브젝트 자신의 타입에 대한 엘리어스(이하 self type으로 지칭)가 필요해졌기에 이래저래 구현을 찾아보니 전부 DEFINE_SELF 투성이다.
결론은 직접 구현해야했고, 성질 급한 여러분을 위해 스니펫을 먼저 제공하자면 다음과 같이 구현했다.

namespace reflection
{
// compile time this_type
namespace detail
{
template <typename Tag>
struct this_type_reader
{
    friend auto this_type(this_type_reader<Tag>);
};

template <typename Tag, typename ThisType>
struct this_type_writer
{
    // inline friend function declaration to exploit ThisType from template x(
    friend auto this_type(this_type_reader<Tag>) { return ThisType{}; }
};

template <typename Tag>
using this_type_read =
    std::remove_pointer_t<decltype(this_type(std::declval<this_type_reader<Tag>>()))>;

} // namespace detail

} // namespace refl

#define DECLARE_TYPE()                                                                   \
public:                                                                                  \
    struct this_type_tag;                                                                \
    constexpr auto this_type_helper()                                                    \
        ->decltype(refl::detail::this_type_writer<this_type_tag, decltype(this)>{},      \
                   void());                                                              \
    using this_type = refl::detail::this_type_read<this_type_tag>;

사실 이거만 보고 이해하는 고인물들은 이 글을 읽을 이유가 없다.
이 아래부터는 그냥 여기서 조금 훔쳐오고 저기서 조금 훔쳐오고 왜 되는지 모르는 걸 이해하는 그런 부분이기 때문이다.

시작

일단 클래스 정의부분에서는 decltype(this)가 되지않는다. 상식적으로 생각해보면 아직 객체가 생성되지도 않았는데 this 포인터가 어떤건지 확인할 방법이 없다. 하지만 또 한편으론 this의 시맨틱이 확실한데 왜 쓰지못하는거지? 라는 의문이 들었다.
아무튼 표준에서 안된다고하면서도 또 trailing return type은 또 괜찮은듯 말하고 있으니... 어떻게 잘 우회해볼 수 있겠다는 생각이 들었다

class Test
{
	auto self_type() -> decltype(this);
};

대략 이런식으로 말이다. 물론 안된다.
근데, 또 표준 드래프트 문서에는 다음과 같은 구절이 있다. decltype안에서 incomplete type을 사용할 수 있다고 한다. 위 예제는 안되는데 그 이유를 찾아보진 않았으니 대강 넘어가자.

If the operand of a decltype-specifier is a prvalue and is not a (possibly parenthesized) immediate invocation ([expr.const]), the temporary materialization conversion is not applied ([conv.rval]) and no result object is provided for the prvalue. The type of the prvalue may be incomplete or an abstract class type.

아이디어

아무튼 그럼 시도할 방법은 decltype안에서 temporary object를 생성하면서 인자로 decltype(this)를 넘겨버리는 것 말고는 생각나지않았다. 근데 이렇게 넘겨준 타입을 어떻게 가져다가 써야하는지에 대한 문제가 남는다. 하지만 C++이 어떤 언어인가! 없는 것 빼고 다 있는 언어니까 잘 찾아보면 뭔가 있을 것이다. 그리고 내가 이 글을 쓴다고 진땀을 빼고 있는 걸 보면 아시겠지만 찾아냈고, 그 이름하야 friend injection이라는 기법이다.
대략 어떤 기법이냐면... 링크를 참조하시는게 좋을 것 같다. 개괄적으로 설명하자면, 메타프로그래밍이 진행되는 도중에 어떤 상태를 캡쳐해서 그 상태를 가져다가 사용하는 것이다. 아 물론 당연히 없어져야할 피쳐이다. 메타프로그래밍은 번역 단위마다 이루어지는 작업인데, 번역단위가 다르면 다른 값을 가져오게 되는 문제가 있다. 또 표준 위원회에서도 제거되어야 할 피쳐라고 써져있긴하지만, 아직 잘 되고, 세심하게 쓴다면 큰 문제는 없으니, 써먹자!

구현

세상은 아무튼 세가지 요소가 필수인거같다. 일단 다음 요소들이 필요한데,
1. 타입을 캡쳐하는 놈
2. 타입을 읽어오는 놈
3. 타입을 캡쳐하는 놈을 선언하는 놈

생각보다 간단하지않은가? 세가지만 있으면 자기 자신을 self라고 지칭할 수 있다. 어디에서든지!
프로토타이핑을 해보자면 대략 다음과 같다.

struct type_reader 
{
	friend auto read_type(type_reader);
};
template <typename ThisType> 
struct type_writer
{
	friend auto read_type(type_reader) { return ThisType{};}
};
using this_type_read = decltype(read_type(std::declval<type_reader>()));

class Test 
{
	auto type_helper() -> decltype(type_writer<decltype(this)>{}, void());
	public:
   using this_type = this_type_read;
};

어라라 컴파일이 안된다.

error: use of ‘auto read_type(type_reader)’ before deduction of ‘auto’

이유는 this_type_read 가 선언되는 즉시, 타입 추론이 일어나기 때문이다.
어떻게 해결할 수 있냐면 별거없다. 템플릿으로 선언해서 인스턴스화 되는 시점에 타입 추론이 일어나도록 하는게 답이다.

코드를 다음과 같이 고치면 문제없이 컴파일 된다.

#include <utility>
#include <type_traits>
struct type_reader 
{
	friend auto read_type(type_reader);
};
template <typename ThisType> 
struct type_writer
{
	friend auto read_type(type_reader) { return ThisType{};}
};

template <typename Tag>
using this_type_read = decltype(read_type(std::declval<type_reader>()));

class Test 
{
	auto type_helper() -> decltype(type_writer<decltype(this)>{}, void());
public:
  using this_type = this_type_read<struct TypeTag>;
};

int main()
{
  Test::this_type T{};
  
  return 0;
}

근데 좀 미심쩍으니까 메인함수에 정적으로 테스트 하나만 추가해보자.

static_assert(std::is_same_v<Test, Test::this_type>);

앗 컴파일이 안된다. 대체 뭐가 문제인고하면 우리는 decltype(this)를 넘겼다. 당연히 Test* 타입으로 추론되어서 전달될 것이고, Test와는 타입이 다른게 맞다!
그럼 단순하게 decltype(*this)를 하면 되겠거니 하면 틀렸다. 타입추론시점에서 Test는 incomplete타입이므로 컴파일 에러가 난다. 그럼 우리는 그냥 Test* 타입과 함께 살아야하는건가? 아니다! 이럴때를 대비해서 표준에서는 std::remove_pointer를 제공한다. 단순히 this_type_read에 라인하나만 더 추가해주면 된다.

#include <utility>
#include <type_traits>
struct type_reader 
{
	friend auto read_type(type_reader);
};
template <typename ThisType> 
struct type_writer
{
	friend auto read_type(type_reader) { return ThisType{};}
};

template <typename Tag>
using this_type_read = std::remove_pointer_t<decltype(read_type(std::declval<type_reader>()))>;

class Test 
{
	auto type_helper() -> decltype(type_writer<decltype(this)>{}, void());
public:
    using this_type = this_type_read<struct TypeTag>;
};

int main()
{
    Test::this_type T{};
    static_assert(std::is_same_v<Test, Test::this_type>);
    
    return 0;
}

이제 완벽하게 컴파일된다!
근데 문제가 하나 있다. 매크로를 이용해서 여러 클래스에서 같은 일을 하면 다음과 같은 컴파일 에러가 뜬다. 당연하게도.

error: redefinition of ‘auto read_type(type_reader)’

우리는 read_type을 여러개 인스턴스화 할 필요가 있다. 일단 처음에는 다음과 같이 태그 디스패치를 이용해서 문제를 풀려고했다.

#include <stdio.h>
#include <utility>
#include <type_traits>

struct type_reader
{
    template <typename Tag>
    friend auto read_type(type_reader, Tag);
};

template <typename ThisType>
struct type_writer
{
    template <typename Tag>
    friend auto read_type(type_reader, Tag)
    {
        return ThisType{};
    }
};

template <typename Tag>
using this_type_read =
    decltype(read_type(std::declval<type_reader>(), std::declval<Tag>()));

class Test
{
public:
    struct TypeTag
    {
    };
    auto type_helper() -> decltype(type_writer<decltype(this)>{}, void());
    using this_type = this_type_read<TypeTag>;
};

int
main()
{
    Test::this_type T{};

    static_assert(std::is_same_v<Test::this_type, Test>);
    return 0;
}

어라라, 정적 어서션이 실패한다. 심지어 Test::this_type이 Test::TypeTag란다.
이유는 간단하다. 템플릿 코드에 충분한 구속을 하지 않아서, 컴파일러가 판단할때 가장 적합한 함수와 객체는 this_type_read<...>에 제공된 typetag로 모두 치환된 객체의 read_type함수가 선택된 것이다.
해결책은 간단하다 함수 템플릿에 달리는 태그를 클래스 템플릿으로 옮기면 된다. 즉 다음과 같이 하면 완성이다.


#include <utility>
#include <type_traits>

template <typename Tag>
struct type_reader
{
    friend auto read_type(type_reader<Tag>);
};

template <typename Tag, typename ThisType>
struct type_writer
{
    friend auto read_type(type_reader<Tag>)
    {
        return ThisType{};
    }
};

template <typename Tag>
using this_type_read =
    std::remove_pointer_t<decltype(read_type(std::declval<type_reader<Tag>>()))>;

class Test
{
public:
    struct TypeTag;
    auto type_helper() -> decltype(type_writer<TypeTag, decltype(this)>{}, void());
    using this_type = this_type_read<TypeTag>;
};

int
main()
{
    Test::this_type T{};

    static_assert(std::is_same_v<Test::this_type, Test>);
    return 0;
}

이걸 보기좋게 다듬고, 매크로를 사용하면 처음에 봤던 코드가 나온다. :)
처음 쓰는 글이라 투박하고 생각이 가는데로 쓴 글인데, 앞으로 차차 수정해보려한다.

profile
C++, 언리얼 개발자입니다. 머신러닝도 조금했었어요.

0개의 댓글