ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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;
    };

    마찬가지로 원시 타입이라면 메모리 복사로 곧바로 읽지만 그게 아니라면 쓰기와 대칭되는 특수화된 읽기 방식이 필요하다.


    -- 계속

    반응형
Designed by Tistory.