데이터 타입과 레이아웃에 대해

네트워크를 통해 데이터를 전송하거나 다른 언어간 데이터를 변환 할 때는 데이터를 직렬화(Serialize)한다. 데이터 타입을 바이너리 스트림으로 변환 하는 것이다. int, double과 같은 스칼라 타입의 경우 형변환 만으로도 직렬화가 가능하다. 이는 메모리에 저장될 때 연속된 공간에 저장 되기 때문이다.

레이아웃(Layout)은 데이터가 메모리에 어떠한 형태로 저장 되는지를 표현한다. C언어의 구조체의 경우도 컴파일러가 정한 규칙에 따라 연속된 공간에 저장되기 때문에 간단한 형변환으로 직렬화 가능하다.

C++특징을 사용한 구조체 들은 어떨까? C++은 가상함수, 상속과 같은 개념이 있고 이는 레이아웃을 복잡하게 만든다. 레이아웃이 복잡하고 고정될 수 없기 때문에 C와 같은 다른 언어로 작성된 프로그램과 호환 될 수 없고, 인접하지 않을 수도 있기 때문에 memcopy와 같은 빠른 하위 수준 함수를 사용할 수 없다. 따라서 Binary I/O 혹은 네트워크 통신에 직접적으로 사용 할 수 없다.

C++14 이러한 타입을 추론하기 위해 간단한 클래스와 구조체인 trivial , 표준 레이아웃 및 POD 또는 Plain Old Data의 세 가지 범주를 도입했습니다.

is_trivial<T>, is_standard_layout<T> 그리고 is_pod<T>를 통해 타입을 추론 할 수 있다.

본 글은 해당 포스트를 번역, 해석하였다.

Standard Layout Type

표준 레이아웃 타입은 C와 호환 가능한 구조체이다. 가상 함수와 같은 C에 없는 특징들을 사용하지 않은 구조체 혹은 클래스이기 떄문이다.

따라서 표준 레이아웃 타입은

표준 레이아웃을 만족하려면 아래의 특성을 만족해야한다.

마지막 항목의 예제는 아래와 같다.

struct Base
{
   int i;
   int j;
};

 

// std::is_standard_layout<Derived> == false!
struct Derived : public Base
{
   int x;
   int y;
};

struct Base
{
   void Foo() {}
};

// std::is_standard_layout<Derived> == true
struct Derived : public Base
{
   int x;
   int y;
};

Trivial Type

Trivial을 번역하면 하찮다는 뜻이 나오는데, 여기서 사용 되는 것은 평함하다, 사소하다는 뜻에 가깝다. 생성자, 소멸자, 복사+이동 생성자가 컴파일러가 제공하는 형태이면 Trivial Type이다.

Trivial Type은

컴파일러가 제공하는 함수를 사용한다는 것은 컴파일러가 메모리에 어떠한 형태로 저장할 수 있는지 결정 할 수 있다는 의미이다. 표준 레이아웃 타입과 달리 데이터 멤버들이 다른 Access Control을 가질 수 있다. 컴파일러가 데이터 멤버의 Access Control의 정보를 알기 때문에 원하는 형태로 메모리에 정렬할 수 있기 때문이다.

따라서 Trivial Type은

Trivial Type은 아래의 조건을 만족해야한다.

아래의 예제는 Trivial Type이다.

struct Trivial
{
   int i;
private:
   int j;
};

struct Trivial2
{
   int i;
   Trivial2(int a, int b) : i(a), j(b) {}
   Trivial2() = default;
private:
   int j;   // Different access control
};

POD types

구조체가 trivial type과 표준 레이아웃을 모두 갖추면 POD(Plain Old Data)라 부른다. POD타입은 연속된 메모리에 저장되고 멤버들이 선언된 순서대로 저장된다.

POD 타입은 바이트 단위로 복사되어 Binary I/O로 수행 될 수 있다. 스칼라 타입들은 POD타입이다.

아래의 예제를 통해 각 데이터 타입들을 체크해보자.

#include <type_traits>
#include <iostream>

using namespace std;

struct B
{
protected:
   virtual void Foo() {}
};

// Neither trivial nor standard-layout
struct A : B
{
   int a;
   int b;
   void Foo() override {} // Virtual function
};

// Trivial but not standard-layout
struct C
{
   int a;
private:
   int b;   // Different access control
};

// Standard-layout but not trivial
struct D
{
   int a;
   int b;
   D() {} //User-defined constructor
};

struct POD
{
   int a;
   int b;
};

int main()
{
   cout << boolalpha;
   cout << "A is trivial is " << is_trivial<A>() << endl; // false
   cout << "A is standard-layout is " << is_standard_layout<A>() << endl;  // false

   cout << "C is trivial is " << is_trivial<C>() << endl; // true
   cout << "C is standard-layout is " << is_standard_layout<C>() << endl;  // false

   cout << "D is trivial is " << is_trivial<D>() << endl;  // false
   cout << "D is standard-layout is " << is_standard_layout<D>() << endl; // true

   cout << "POD is trivial is " << is_trivial<POD>() << endl; // true
   cout << "POD is standard-layout is " << is_standard_layout<POD>() << endl; // true

   return 0;
}