데이터 멤버

데이터 멤버 (Data members)

객체가 실제로 담는 데이터를 비정적 데이터 멤버(non-static data member)라고 불러요. 클래스의 멤버 명세(member specification) 안에 선언돼요.

출처: cppreference

본문

비정적 데이터 멤버는 클래스의 멤버 명세에 선언돼요.

class S
{
    int n;              // non-static data member
    int& r;             // non-static data member of reference type
    int a[2] = {1, 2};  // non-static data member with default member initializer (C++11)
    std::string s, *ps; // two non-static data members
    
    struct NestedS
    {
        std::string s;
    } d5;               // non-static data member of nested type
    
    char bit : 2;       // two-bit bitfield
};

간단한 선언은 모두 허용되지만, 다음은 제외돼요:

  • externregister 저장 클래스 지정자는 허용되지 않아요.
참고 (since)
thread_local 저장 클래스 지정자는 허용되지 않는다 (단 정적 데이터 멤버에는 허용됨). (since C++11)
  • 불완전 타입, 추상 클래스 타입, 그리고 그 배열은 허용되지 않아요: 특히 클래스 C는 타입 C의 비정적 데이터 멤버를 가질 수 없지만, 타입 C&(C에 대한 참조)나 C*(C에 대한 포인터)의 비정적 데이터 멤버는 가질 수 있어요.
  • 사용자 선언 생성자가 하나라도 있으면 비정적 데이터 멤버는 클래스의 이름과 같은 이름을 가질 수 없어요.
참고 (since)
자리표시자 타입 지정자(즉 auto, decltype(auto)(since C++14), 추론 대상인 클래스 템플릿 이름(since C++17), 제한된 자리표시자(since C++20))는 비정적 데이터 멤버 선언에 쓸 수 없다 (단 클래스 정의에서 초기화되는 정적 데이터 멤버에는 허용됨). (since C++11)

추가로 비트 필드(bit-field) 선언이 허용돼요.

배치 (Layout)

어떤 클래스 C의 객체가 생성되면, 비참조 타입의 각 비정적 데이터 멤버는 C의 객체 표현(object representation)의 어떤 부분에 할당돼요. 참조 멤버가 어떤 저장 공간을 차지하는지는 구현이 정의하지만, 그 저장 기간은 그들이 멤버인 객체와 같아요.

규칙 (since)
비유니온 클래스 타입에 대해, 접근 지정자로 구분되지 않은(until C++11)같은 멤버 접근을 가진(since C++11) 비-제로 크기(since C++20) 멤버는 항상 나중에 선언된 멤버가 클래스 객체 안에서 더 높은 주소를 갖도록 할당된다. 접근 지정자로 구분된(until C++11)다른 접근 제어를 가진(since C++11) 멤버는 불특정 순서로 할당된다 (컴파일러는 그것들을 함께 묶을 수 있음). (until C++23)
비유니온 클래스 타입에 대해, 비-제로 크기 멤버는 항상 나중에 선언된 멤버가 클래스 객체 안에서 더 높은 주소를 갖도록 할당된다. 멤버의 접근 제어가 여전히 standard-layout 속성에 영향을 준다는 점을 참고 (아래 참고). (since C++23)

정렬 요구 사항은 멤버 사이 또는 클래스의 마지막 멤버 뒤에 패딩을 필요로 할 수 있어요.

Standard-layout

연결 (since)
모든 비정적 데이터 멤버가 같은 접근 제어를 갖고 특정 다른 조건을 충족하는 클래스는 standard-layout 클래스 알려져 있다 (요구 사항 목록은 standard-layout class 참고). (until C++11)(since C++11)

두 standard-layout 비유니온 클래스 타입의 공통 초기 시퀀스(common initial sequence) 는, 각 클래스의 첫 번째 그런 엔터티부터 시작해 선언 순서대로 비정적 데이터 멤버와 비트 필드의 가장 긴 시퀀스인데, 다음이 성립해요:

참고 (since)
__has_cpp_attribute(no_unique_address)가 0이 아니면 어느 엔터티도 [[no_unique_address]] 속성으로 선언되지 않는다. (since C++20)
  • 대응하는 엔터티가 배치 호환(layout-compatible) 타입을 가짐
  • 대응하는 엔터티가 같은 정렬 요구 사항을 가짐
  • 둘 다 같은 너비의 비트 필드이거나 둘 다 비트 필드가 아님
struct A { int a; char b; };
struct B { const int b1; volatile char b2; }; 
// A and B's common initial sequence is A.a, A.b and B.b1, B.b2

struct C { int c; unsigned : 0; char b; };
// A and C's common initial sequence is A.a and C.c

struct D { int d; char b : 4; };
// A and D's common initial sequence is A.a and D.d

struct E { unsigned int e; char b; };
// A and E's common initial sequence is empty

두 standard-layout 비유니온 클래스 타입은 cv 한정자를 무시하고 같은 타입이거나, 배치 호환 열거형(즉 같은 기본 타입을 가진 열거형)이거나, 그 공통 초기 시퀀스가 모든 비정적 데이터 멤버와 비트 필드로 구성되면 배치 호환(layout-compatible)이라고 불러요 (위 예에서 AB는 배치 호환).

두 standard-layout 유니온은 같은 수의 비정적 데이터 멤버를 갖고 대응하는 비정적 데이터 멤버(어떤 순서로든)가 배치 호환 타입을 가지면 배치 호환이라고 불러요.

standard-layout 타입은 다음과 같은 특별한 속성을 가져요:

  • 비유니온 클래스 타입 T1의 활성 멤버를 가진 standard-layout 유니온에서, mT1T2의 공통 초기 시퀀스의 일부라면 다른 유니온 멤버의 비유니온 클래스 타입 T2의 비정적 데이터 멤버 m을 읽는 것이 허용돼요 (비-volatile glvalue를 통해 volatile 멤버를 읽는 것은 undefined라는 점 제외).
  • standard-layout 클래스 타입의 객체에 대한 포인터는 (비정적 데이터 멤버가 있으면) 첫 번째 비정적 비비트 필드 데이터 멤버에 대한, (있으면) 그 외에 어떤 기반 클래스 부분 객체에 대한 포인터로 reinterpret_cast될 수 있고 그 반대도 가능해요. 즉 standard-layout 타입의 첫 번째 데이터 멤버 앞에는 패딩이 허용되지 않아요. 그러한 캐스트 결과에는 엄격한 앨리어싱 규칙이 여전히 적용된다는 점을 참고해요.
  • offsetof 매크로를 사용해 standard-layout 클래스의 시작에서 임의 멤버의 오프셋을 결정할 수 있어요.

멤버 초기화 (Member initialization)

비정적 데이터 멤버는 두 가지 방법 중 하나로 초기화될 수 있어요:

  1. 생성자의 멤버 초기화 목록(member initializer list)에서.
struct S
{
    int n;
    std::string s;
    S() : n(7) {} // direct-initializes n, default-initializes s
};
  1. 클래스 정의에서 기본 멤버 초기화식(default member initializer)으로.
참고 (since)
기본 멤버 초기화식은 비트 필드 멤버에 허용되지 않는다. (until C++20)(since C++11)

배열 타입의 멤버는 멤버 초기화식으로부터 크기를 추론할 수 없어요:

struct X
{
    int a[] = {1, 2, 3};  // error
    int b[3] = {1, 2, 3}; // OK
};

기본 멤버 초기화식은 둘러싸는 클래스의 defaulted 기본 생성자 또는 그 생성자의 예외 명세의 암시적 정의를 일으키는 것이 허용되지 않아요:

struct node
{
    node* p = new node; // error: use of implicit or defaulted node::node() 
};

참조 멤버는 기본 멤버 초기화식에서 임시에 바인딩될 수 없어요 (멤버 초기화 목록에도 같은 규칙이 존재함):

struct A
{
    A() = default;     // OK
    A(int v) : v(v) {} // OK
    const int& v = 42; // OK
};

A a1;    // error: ill-formed binding of temporary to reference
A a2(1); // OK (default member initializer ignored because v appears in a constructor)
         // however a2.v is a dangling reference
참고 (since)
참조 멤버가 기본 멤버 초기화식에서 초기화되는 경우(until C++20)멤버가 기본 멤버 초기화식을 가질 경우(since C++20) 그 초기화식의 잠재적으로 평가되는 부분 표현식이 그 기본 멤버 초기화식을 사용할 집합체 초기화이면 프로그램은 ill-formed이다. (since C++17)

사용 (Usage)

비정적 데이터 멤버나 비정적 멤버 함수의 이름은 다음 세 가지 상황에서만 나타날 수 있어요:

  1. 클래스 멤버 접근 표현식의 일부로, 그 클래스가 이 멤버를 갖거나 이 멤버를 가진 클래스에서 파생된 경우. 여기에는 this가 허용된 어떤 문맥(멤버 함수 본문, 멤버 초기화 목록, 클래스 내 기본 멤버 초기화식)에서 비정적 멤버 이름이 쓰일 때 나타나는 암시적 this-> 멤버 접근 표현식이 포함돼요.
struct S
{
    int m;
    int n;
    int x = m;            // OK: implicit this-> allowed in default initializers (C++11)
    
    S(int i) : m(i), n(m) // OK: implicit this-> allowed in member initializer lists
    {
        this->f();        // explicit member access expression
        f();              // implicit this-> allowed in member function bodies
    }
    
    void f();
};
  1. 비정적 멤버에 대한 포인터를 만들기 위해.
struct S
{
    int m;
    void f();
};

int S::*p = &S::m;       // OK: use of m to make a pointer to member
void (S::*fp)() = &S::f; // OK: use of f to make a pointer to member
  1. (데이터 멤버에만, 멤버 함수에는 아님) 미평가 피연산자(unevaluated operands)에서 사용될 때.
struct S
{
    int m;
    static const std::size_t sz = sizeof m; // OK: m in unevaluated operand
};

std::size_t j = sizeof(S::m + 42); // OK: even though there is no "this" object for m

참고: 이런 사용은 N2253의 CWG 이슈 613 해결을 통해 허용되며, 일부 컴파일러(예: clang)는 이것을 C++11의 변경으로 취급해요.

참고 (Notes)

Feature-test 매크로 표준 기능
__cpp_nsdmi 200809L (C++11) Non-static data member initializers
__cpp_aggregate_nsdmi 201304L (C++14) Aggregate classes with default member initializers

더 알아보기 (Learn more)

  • 클래스 — 데이터 멤버와 멤버 함수를 묶는 타입.
  • 정적 멤버 — 클래스에 종속되지만 객체별로는 존재하지 않는 멤버.
  • 비정적 멤버 함수 — 객체에 대해 호출되는 멤버 함수.
  • std::is_standard_layout (C++11) — 타입이 standard-layout 타입인지 검사하는 클래스 템플릿.
  • offsetof — standard-layout 타입의 시작에서 지정된 멤버까지의 바이트 오프셋을 얻는 함수 매크로.