프로그래밍/c++
c++에서 리플렉션 흉내내기
제페
2018. 1. 3. 01:11
반응형
원시 타입을 구분할 enum을 만든다.
enum class PrimitiveType
{
Char = 1,
String,
Int16,
Int32,
Int64,
Real32,
Real64,
};
멤버 변수 하나를 표현하는 클래스를 만든다.
class MemberVariable
{
public:
MemberVariable(const char* name, PrimitiveType type, std::uint16_t offset) :
name(name),
type(type),
offset(offset)
{
}
const char* GetName() const
{
return name.c_str();
}
PrimitiveType GetType() const
{
return type;
}
unsigned int GetOffset() const
{
return offset;
}
private:
std::string name;
PrimitiveType type;
std::uint16_t offset;
};
변수의 집합을 표현하는 클래스를 만든다.
class DataType
{
public:
template < typename Iterator >
DataType(Iterator begin, Iterator end) :
memberVariables(begin, end)
{
}
DataType(std::initializer_list<MemberVariable> memberVariables) :
DataType(memberVariables.begin(), memberVariables.end())
{
}
const std::vector<MemberVariable>& GetMemberVariables() const
{
return memberVariables;
}
private:
std::vector<MemberVariable> memberVariables;
};
이제 DataType으로 클래스를 내부 구조를 표현할 수 있다.
Student란 클래스가 있다고 하자. 이 클래스는 이름, 나이, 다리 개수를 멤버 변수로 가진다.
class Student
{
public:
std::string name = "Jeon";
int age = 27;
int legCount = 3;
};
여기에 내부 구조를 표현하도록 하면
class Student
{
public:
std::string name = "Jeon";
int age = 27;
int legCount = 3;
static const DataType& GetDataType()
{
static const DataType dataType{
{ "name", PrimitiveType::String, offsetof(Student, name) },
{ "age", PrimitiveType::Int32, offsetof(Student, age) },
{ "legCount", PrimitiveType::Int32, offsetof(Student, legCount) },
};
return dataType;
}
};
이렇게 표현 할 수 있겠다.
0. 리플렉션 활용해서 직렬화하기
개체를 메모리에 쓰는 헬퍼 클래스를 만든다.
#pragma once
#include <vector>
#include <cstddef>
class OutputMemoryStream
{
public:
OutputMemoryStream()
{
buffer.resize(32);
}
std::pair<const std::byte*, size_t> GetBufferData() const
{
return { buffer.data(), head };
}
const std::byte* GetBufferPointer() const
{
return buffer.data();
}
size_t GetBufferSize() const
{
return head;
}
void Write(const void* data, size_t size)
{
Reserve(head + size);
std::memcpy(buffer.data() + head, data, size);
head = head + size;
}
template < typename Type >
void Write(const Type& value)
{
// 원시 자료형 여부 검사
static_assert(
std::is_arithmetic_v<Type> ||
std::is_enum_v<Type>,
"generic write only supports primitive data types");
Write(&value, sizeof(value));
}
void Reserve(std::size_t size)
{
buffer.reserve(head + size);
}
private:
std::vector<std::byte> buffer;
size_t head = 0;
};
핵심은 타입에 따라 메모리에 쓰는 방식을 구분하는 것이다.
메모리에 곧바로 쓰고 읽을 수 있는 int, float, char 같은 원시 타입이라면 곧바로 복사를 하지만 그게 아니라면 특수화된 처리가 필요하다.
template <>
void Write<std::string>(const std::string& str)
{
Write((std::uint32_t)str.length());
Write(str.data(), str.length());
}
이런식으로.
void Serialize(OutputMemoryStream& stream, const DataType& dataType, const void* src)
{
for (const auto& memberVariable : dataType.GetMemberVariables())
{
std::byte* p = (std::byte*)src + memberVariable.GetOffset();
switch (memberVariable.GetType())
{
case PrimitiveType::Int32:
stream.Write(*(std::int32_t*)p);
break;
case PrimitiveType::Int64:
stream.Write(*(std::int64_t*)p);
break;
case PrimitiveType::Real32:
stream.Write(*(std::float_t*)p);
break;
case PrimitiveType::Real64:
stream.Write(*(std::double_t*)p);
break;
case PrimitiveType::String:
stream.Write(*(std::string*)p);
break;
}
}
}
이제 메모리에서 읽는 헬퍼 클래스를 만든다.
#pragma once
#include <cstddef>
#include <vector>
class InputMemoryStream
{
public:
InputMemoryStream(const void* data, std::size_t size);
~InputMemoryStream();
void Read(void* dest, std::size_t size);
template < typename Type >
void Read(Type& out)
{
// 원시 자료형 여부 검사
static_assert(
std::is_arithmetic_v<Type> ||
std::is_enum_v<Type>,
"generic read only supports primitive data types");
Read(&out, sizeof(Type));
}
template <>
void Read<std::string>(std::string& out)
{
std::uint32_t length;
Read(length);
std::string temp;
temp.resize(length);
Read(temp.data(), length);
out = std::move(temp);
}
private:
const void* data;
std::size_t size;
std::size_t index;
};
마찬가지로 원시 타입이라면 메모리 복사로 곧바로 읽지만 그게 아니라면 쓰기와 대칭되는 특수화된 읽기 방식이 필요하다.
-- 계속
반응형