멤버 접근 연산자

멤버 접근 연산자 (Member access operators)

멤버 접근 연산자는 피연산자의 멤버에 접근해요. 배열 첨자([]), 역참조(*), 주소(&), 멤버 접근(., ->), 멤버 포인터(.*, ->*)까지 한자리에서 다루는 주제예요.

출처: cppreference - Member access operators

본문

연산자 이름 문법 오버로드 가능 클래스 안 프로토타입 예 (T) 클래스 밖 프로토타입 예 (T)
첨자 (subscript) a[b] R& T::operator[](S b); N/A
첨자 (멀티, C++23) a[...] R& T::operator[](...);
역참조 (indirection) *a R& T::operator*(); R& operator*(T a);
주소 (address-of) &a R* T::operator&(); R* operator&(T a);
객체의 멤버 a.b 아니요 N/A N/A
포인터의 멤버 a->b R* T::operator->(); N/A
객체의 멤버 포인터 a.*b 아니요 N/A N/A
포인터의 멤버 포인터 a->*b R& T::operator->*(S b); R& operator->*(T a, S b);

참고:

  • 대부분의 사용자 정의 오버로드처럼, 반환 타입은 내장 연산자가 제공하는 반환 타입과 맞춰야 사용자 정의 연산자를 내장처럼 쓸 수 있어요. 다만 사용자 정의 오버로드에서는 반환 타입으로 어떤 타입이든(void 포함) 쓸 수 있어요. 예외 하나는 operator->인데, 실제로 쓸 수 있으려면 포인터나 오버로드된 operator->를 가진 다른 클래스를 반환해야 해요.

설명 (Explanation)

  • 내장 첨자 연산자는 포인터나 배열 피연산자가 가리키는 객체에 접근해요.
  • 내장 역참조 연산자는 포인터 피연산자가 가리키는 객체나 함수에 접근해요.
  • 내장 주소 연산자는 객체나 함수 피연산자를 가리키는 포인터를 만들어요.
  • 객체의 멤버·객체의 멤버 포인터 연산자는 객체 피연산자의 데이터 멤버나 멤버 함수에 접근해요.
  • 내장 포인터의 멤버·포인터의 멤버 포인터 연산자는 포인터 피연산자가 가리키는 클래스의 데이터 멤버나 멤버 함수에 접근해요.

내장 첨자 연산자

첨자 연산자 표현식은 다음 형태를 가져요:

형태 설명
expr1[expr2] (1) 내장 연산자
expr1[{expr, ...}] (2) (C++11 이후) 대괄호 안 중괄호 목록 형태. 오버로드된 operator[]를 호출할 때만 쓰여요.
expr1[expr2, expr, ...] (3) (C++23 이후) 대괄호 안 콤마 구분 표현식 목록. 오버로드된 operator[]를 호출할 때만 쓰여요.
  1. 내장 연산자에서 expr1expr2 중 하나는 "T 배열" 타입의 glvalue이거나 "T에 대한 포인터" 타입의 prvalue여야 하고, 다른 하나는 unscoped 열거형이나 정수 타입의 prvalue여야 해요. 이 표현식의 결과 타입은 T예요. expr2는 괄호로 감싸지 않은 콤마 표현식일 수 없어요. (C++23 이후)

내장 첨자 표현식 E1[E2]는 값 범주(아래 참고)와 평가 순서(C++17 이후)를 제외하면 표현식 *(E1 + E2)와 정확히 동일해요. 포인터 피연산자(배열-포인터 변환의 결과일 수 있고, 어떤 배열의 요소나 그 끝 뒤를 가리켜야 해요)는 포인터 산술 규칙에 따라 같은 배열의 다른 요소를 가리키도록 조정된 뒤 역참조돼요.

배열에 적용될 때, 배열이 lvalue면 첨자 표현식은 lvalue이고, 아니면 xvalue예요(C++11 이후).

포인터에 적용될 때 첨자 표현식은 항상 lvalue예요.

&x[0]처럼 T의 크기나 내부 구조를 전혀 쓰지 않더라도 타입 T가 불완전 타입(incomplete type)이면 안 돼요.

첨자 연산자의 두 번째(오른쪽) 인자로 괄호로 감싸지 않은 콤마 표현식을 쓰는 것은 폐기 예정이에요. 예를 들어 a[b,c]는 폐기 예정이고 a[(b,c)]는 아니에요. (C++20 이후, until C++23)

괄호로 감싸지 않은 콤마 표현식은 첨자 연산자의 두 번째(오른쪽) 인자가 될 수 없어요. 예를 들어 a[b,c]는 ill-formed이거나 a.operator[](b,c)와 동등해요. 콤마 표현식을 첨자로 쓰려면 a[(b,c)]처럼 괄호가 필요해요. (C++23 이후)

사용자 정의 연산자와의 오버로드 결정에서, 모든 (가능하면 cv 한정된) 객체 타입 T에 대해 다음 함수 시그니처가 참여해요:

T& operator[](T*, std::ptrdiff_t);
T& operator[](std::ptrdiff_t, T*);
#include <iostream>
#include <map>
#include <string>

int main()
{
    int a[4] = {1, 2, 3, 4};
    int* p = &a[2];
    std::cout << p[1] << p[-1] << 1[p] << (-1)[p] << '\n';

    std::map<std::pair<int, int>, std::string> m;
    m[{1, 2}] = "abc"; // uses the [{...}] version
}

출력:

4242

E1[E2]*(E1+E2)와 같기 때문에, p[1]p가 가리키는 요소(a[2])의 다음 요소 a[3](=4), p[-1]a[1](=2), 1[p]*(1+p)a[3](=4), (-1)[p]a[1](=2)예요. 그래서 4242가 출력돼요. 첨자가 교환적(commutative)이라는 흥미로운 특성이 드러나죠.

내장 역참조 연산자

역참조 연산자 표현식은 다음 형태를 가져요:

*expr

내장 역참조 연산자의 피연산자는 객체에 대한 포인터나 함수에 대한 포인터여야 하고, 결과는 expr이 가리키는 객체나 함수를 가리키는 lvalue예요. expr이 실제로 객체나 함수를 가리키지 않으면 동작이 정의되지 않아요. (typeid가 지정하는 경우는 예외예요.)

(가능하면 cv 한정된) void에 대한 포인터는 역참조할 수 없어요. 다른 불완전 타입에 대한 포인터는 역참조할 수 있지만, 결과 lvalue는 참조 초기화처럼 불완전 타입의 lvalue를 허용하는 문맥에서만 쓸 수 있어요.

사용자 정의 연산자와의 오버로드 결정에서, 객체 타입(가능하면 cv 한정)이나 함수 타입(const·ref 한정이 아닌)인 모든 타입 T에 대해 다음 함수 시그니처가 참여해요:

T& operator*(T*);
#include <iostream>

int f() { return 42; }

int main()
{
    int n = 1;
    int* pn = &n;

    int& r = *pn; // lvalue can be bound to a reference
    int m = *pn;  // indirection + lvalue-to-rvalue conversion

    int (*fp)() = &f;
    int (&fr)() = *fp; // function lvalue can be bound to a reference

    [](...){}(r, m, fr); // removes possible "unused variable" warnings
}

내장 주소 연산자

주소 연산자 표현식은 다음 형태를 가져요:

형태 설명
&expr (1)
&class::member (2)
&splice-specifier (3, C++26 이후)
  1. 피연산자가 어떤 객체나 함수 타입 T의 lvalue 표현식이면, operator&는 피연산자가 나타내는 객체나 함수를 가리키는, 같은 cv 한정을 가진 T* 타입의 prvalue를 만들고 돌려줘요. 피연산자가 불완전 타입이면 포인터를 만들 수 있어요. 다만 그 불완전 타입이 자신만의 operator&를 정의하는 클래스라면 내장 연산자를 쓸지 오버로드를 쓸지 불특정이에요. 사용자 정의 operator&를 가진 타입의 피연산자에 대해 std::addressof를 쓰면 진짜 포인터를 얻을 수 있어요.

참고로 C99 이후의 C 버전과 달리, 단항 * 연산자의 결과에 적용되는 단항 &에 대한 특별한 경우는 없어요.

expr이 명시적 객체 멤버 함수(explicit object member function)를 가리키면 expr은 자격을 갖춘 식별자(qualified identifier)여야 해요. 명시적 객체 멤버 함수를 가리키는 비한정 식별자에 &를 적용하는 건 ill-formed예요. (C++23 이후)

  1. 피연산자가 명시적 객체 멤버 함수(C++23 이후)가 아닌 비정적·변형(variant) 멤버의 자격을 갖춘 이름, 예를 들어 &C::member이면 결과는 클래스 C에서 타입 T의 멤버 함수 포인터나 데이터 멤버 포인터인 prvalue예요. &memberC::member도, 심지어 &(C::member)도 멤버 포인터를 초기화하는 데 쓸 수 없다는 점에 주의하세요.

  2. 피연산자가 스플라이스 지정자(splice specifier)이면, 비정적 멤버를 나타내면 결과는 멤버 포인터이고, 그 외에는 지정된 객체·함수를 가리키는 객체 포인터·함수 포인터예요. 괄호로 감싸지 않은 스플라이스 지정자가 익명 유니온의 멤버를 나타내면 그 표현식은 유효하지 않아요. &([:r:])는 결코 멤버 포인터가 아니라는 점을 주의하세요.

피연산자가 오버로드된 함수나 함수 템플릿을 나타내면, 문맥 때문에 오버로드를 결정할 수 있을 때만 주소를 취할 수 있어요. 오버로드된 함수의 주소를 참고하세요.

사용자 정의 연산자와의 오버로드 결정에서 이 연산자는 추가 함수 시그니처를 도입하지 않아요. viable 함수인 오버로드된 operator&가 존재하면 내장 주소 연산자는 적용되지 않아요.

void f(int) {}
void f(double) {}

struct A { int i; };
struct B { void f(); };

int main()
{
    int n = 1;
    int* pn = &n;    // pointer
    int* pn2 = &*pn; // pn2 == pn

    int A::* mp = &A::i;      // pointer to data member
    void (B::*mpf)() = &B::f; // pointer to member function

    void (*pf)(int) = &f; // overload resolution due to initialization context
//  auto pf2 = &f; // error: ambiguous overloaded function type
    auto pf2 = static_cast<void (*)(int)>(&f); // overload resolution due to cast
}

내장 멤버 접근 연산자

멤버 접근 연산자 표현식은 다음 형태를 가져요:

형태 설명
expr.template(optional) id-expr (1)
expr->template(optional) id-expr (2)
expr.splice-specifier (3, C++26 이후)
expr->splice-specifier (3, C++26 이후)
  1. id-expr이 의사 소멸자(pseudo-destructor)를 가리키지 않으면(아래 참고), expr은 완전 클래스 타입 T의 표현식이어야 해요. id-expr이 정적 멤버나 열거자를 가리키면 expr은 무시된 값 표현식(discarded-value expression)이에요.

  2. id-expr이 의사 소멸자를 가리키지 않으면, expr은 완전 클래스 타입 T*에 대한 포인터의 표현식이어야 해요.

id-exprT의 또는 T의 모호하지 않고 접근 가능한 기반 클래스 B의 멤버를 가리키는 이름(형식적으로는 그런 멤버를 명명하는 식별자 표현식)이에요. 예: E1.E2E1->E2, 선택적으로 자격을 갖추고(E1.B::E2E1->B::E2), 선택적으로 template 구분자를 쓸 수 있어요(E1.template E2E1->template E2).

splice-specifierT의 또는 T의 모호하지 않고 접근 가능한 기반 클래스 B의 멤버, 또는 직접 기반 클래스 관계(즉 std::meta::bases_of(^^T, ctx)의 요소)를 나타내는 스플라이스 표현식이에요. (C++26 이후)

사용자 정의 operator->가 호출되면, 반환된 값에 operator->가 재귀적으로 다시 호출돼요. 드디어 평범한 포인터를 반환하는 operator->에 도달할 때까지요. 그 후 그 포인터에 내장 의미론이 적용돼요.

내장 타입에서 E1->E2(*E1).E2와 정확히 동등해요. 그래서 아래 규칙은 E1.E2만 다뤄요.

E1.E2 표현식에서:

  1. E2가 정적 데이터 멤버면:

    • E2가 참조 타입 T&T&&(C++11 이후)이면, 결과는 참조가 바인딩된 객체나 함수를 나타내는 T 타입의 lvalue예요.
    • 그 외에 E2의 타입을 T라고 하면, 결과는 그 정적 데이터 멤버를 나타내는 T 타입의 lvalue예요.
    • 두 경우 모두 E1은 평가되고 무시돼요.
  2. E2가 비정적 데이터 멤버면:

    • E2가 참조 타입 T&T&&(C++11 이후)이면, 결과는 E1의 해당 참조 멤버가 바인딩된 객체나 함수를 나타내는 T 타입의 lvalue예요.
    • 그 외에 E1이 lvalue면 결과는 E1의 그 비정적 데이터 멤버를 나타내는 lvalue예요.
    • 그 외에 (E1이 rvalue(until C++17)/xvalue(prvalue에서 구체화될 수 있음, C++17 이후)면) 결과는 E1의 그 비정적 데이터 멤버를 나타내는 rvalue(until C++11)/xvalue(C++11 이후)예요. E2mutable 멤버가 아니면 결과의 cv 한정은 E1E2의 cv 한정의 합집합이고, E2mutable 멤버면 E1E2volatile 한정의 합집합이에요.
  3. E2가 오버로드 집합(정적 멤버 함수와 비정적 멤버 함수 하나 이상)이면, E1.E2는 (가능하면 괄호로 감싼) 멤버 함수 호출 연산자의 왼쪽 피연산자여야 해요. 그리고 함수 오버로드 결정으로 E2가 가리키는 함수를 선택해요. 그 후:

    • E2가 정적 멤버 함수면 결과는 그 정적 멤버 함수를 나타내는 lvalue예요. 이 경우 E1은 평가되고 무시돼요.
    • 그 외에 (E2가 비정적 멤버 함수면) 결과는 E1의 그 비정적 멤버 함수를 나타내는 prvalue예요.
  4. E2가 멤버 열거자면, E2의 타입을 T라고 할 때 결과는 그 열거자의 값을 값으로 가진 T 타입의 rvalue(until C++11)/prvalue(C++11 이후)예요.

  5. E2가 중첩 타입(nested type)이면 프로그램은 ill-formed예요.

  6. E2가 직접 기반 클래스 관계를 나타내면, 결과는 E1의 그 직접 기반 클래스 부분객체를 나타내요. (C++26 이후)

struct B { int b; };
struct C : B { int get() const { return b; } };
struct D : B, C {};
constexpr int f() {
    D d = {1, {}};

    constexpr auto ctx = std::meta::access_context::current();

    // b unambiguously refers to the direct base class of type B,
    // not the indirect base class of type B
    B& b = d.[: std::meta::bases_of(^^D, ctx)[0] :];
    b.b += 10;
    return 10 * b.b + d.get();
}

static_assert(f() == 110);

(C++26 이후)

  1. E1이 스칼라 타입이고 E2가 그와 같은 타입(cv 한정 제외)을 나타내는 ~ 뒤의 타입 이름이나 decltype 지정자(선택적으로 자격을 갖춘)이면, 결과는 특별한 종류의 prvalue예요. 함수 호출 연산자의 왼쪽 피연산자로만 쓸 수 있고 다른 용도는 없어요. 결과로 만들어지는 함수 호출 표현식을 **의사 소멸자 호출(pseudo-destructor call)**이라고 해요. 인자를 받지 않고 void를 반환하며, E1을 평가하고 결과 객체의 수명을 끝내요. . 연산자의 왼쪽 피연산자가 비클래스 타입을 가질 수 있는 유일한 경우예요. 의사 소멸자 호출을 허용하면 주어진 타입에 소멸자가 존재하는지 알 필요 없이 코드를 쓸 수 있어요.

operator.은 오버로드할 수 없어요. operator->에 대해, 사용자 정의 연산자와의 오버로드 결정에서 내장 연산자는 추가 함수 시그니처를 도입하지 않아요. viable 함수인 오버로드된 operator->가 존재하면 내장 operator->는 적용되지 않아요.

#include <cassert>
#include <iostream>
#include <memory>

struct P
{
    template<typename T>
    static T* ptr() { return new T; }
};

template<typename T>
struct A
{
    A(int n): n(n) {}

    int n;
    static int sn;

    int f() { return 10 + n; }
    static int sf() { return 4; }

    class B {};
    enum E {RED = 1, BLUE = 2};

    void g()
    {
        typedef int U;

        // keyword template needed for a dependent template member
        int* p = T().template ptr<U>();
        p->~U(); // U is int, calls int's pseudo destructor
        delete p;
    }
};

template<>
int A<P>::sn = 2;

struct UPtrWrapper
{
    std::unique_ptr<std::string> uPtr;
    std::unique_ptr<std::string>& operator->() { return uPtr; }
};

int main()
{
    A<P> a(1);
    std::cout << a.n << ' '
              << a.sn << ' '   // A::sn also works
              << a.f() << ' '
              << a.sf() << ' ' // A::sf() also works
//            << &a.f << ' '   // error: ill-formed if a.f is not the
                               // left-hand operand of operator()
//            << a.B << ' '    // error: nested type not allowed
              << a.RED << ' '; // enumerator

    UPtrWrapper uPtrWrap{std::make_unique<std::string>("wrapped")};
    assert(uPtrWrap->data() == uPtrWrap.operator->().operator->()->data());
}

출력:

1 2 11 4 1

여기서 a.f()처럼 클래스 안에서 정의된 멤버 함수 호출, a.sf()처럼 정적 멤버 함수, a.RED처럼 열거자가 접근되는 모습을 볼 수 있어요. a.B처럼 중첩 타입은 E1.E2로 접근할 수 없어서 오류예요. 그리고 UPtrWrapperoperator->std::unique_ptr을 반환하는데, 드디어 평범한 포인터(std::string*)를 반환하는 unique_ptr::operator->에 도달할 때까지 재귀적으로 호출돼요.

E2가 비정적 멤버나 직접 기반 클래스 관계(C++26 이후)이고 E1의 결과가 E1의 타입과 유사하지 않은 타입의 객체면 동작이 정의되지 않아요:

struct A { int i; };
struct B { int j; };
struct D : A, B {};

void f()
{
    D d;
    static_cast<B&>(d).j;      // OK, object expression designates the B subobject of d
    reinterpret_cast<B&>(d).j; // undefined behavior
}

내장 멤버 포인터 접근 연산자

멤버 포인터를 통한 멤버 접근 연산자 표현식은 다음 형태를 가져요:

형태 설명
lhs .* rhs (1) lhs는 클래스 타입 T의 표현식이어야 해요
lhs ->* rhs (2) lhs는 클래스 타입 포인터 T*의 표현식이어야 해요

rhsT의, 또는 T의 모호하지 않고 접근 가능한 기반 클래스 B의 (데이터 또는 함수) 멤버 포인터 타입의 rvalue여야 해요.

내장 타입에서 E1->*E2(*E1).*E2와 정확히 동등해요. 그래서 아래 규칙은 E1.*E2만 다뤄요.

E1.*E2 표현식에서:

  1. E2가 데이터 멤버 포인터면:
    • E1이 lvalue면 결과는 그 데이터 멤버를 나타내는 lvalue예요.
    • 그 외에 (E1이 rvalue(until C++17)/xvalue(prvalue에서 구체화될 수 있음, C++17 이후)면) 결과는 그 데이터 멤버를 나타내는 rvalue(until C++11)/xvalue(C++11 이후)예요.
  2. E2가 멤버 함수 포인터면, 결과는 그 멤버 함수를 나타내는 특별한 종류의 prvalue예요. 멤버 함수 호출 연산자의 왼쪽 피연산자로만 쓸 수 있고 다른 용도는 없어요.
  3. cv 한정 규칙은 객체의 멤버 연산자와 같아요. 한 가지 추가 규칙: mutable 멤버를 가리키는 멤버 포인터는 const 객체에서 그 멤버를 수정하는 데 쓸 수 없어요.
  4. E2가 null 멤버 포인터 값이면 동작이 정의되지 않아요.
  5. E1의 결과가 E1의 타입과 유사하지 않은 타입의 객체이거나, 그 가장 파생된 객체가 E2가 가리키는 멤버를 포함하지 않으면 동작이 정의되지 않아요.
  6. E1이 rvalue이고 E2가 ref 한정자 &가 붙은 멤버 함수를 가리키면, 멤버 함수에 const(단 volatile은 아님) cv 한정자가 없는 한 프로그램은 ill-formed예요. (C++20 이후)
  7. E1이 lvalue이고 E2가 ref 한정자 &&가 붙은 멤버 함수를 가리키면 프로그램은 ill-formed예요. (C++11 이후)

사용자 정의 연산자와의 오버로드 결정에서, 클래스 타입 BD와 같거나 D의 모호하지 않고 접근 가능한 기반 클래스이고 R이 객체나 함수 타입인 모든 타입 조합 D, B, R에 대해 다음 함수 시그니처가 참여해요:

R& operator->*(D*, R B::*);

여기서 두 피연산자 모두 cv 한정될 수 있고, 그 경우 반환 타입의 cv 한정은 피연산자의 cv 한정의 합집합이에요.

#include <iostream>

struct S
{
    S(int n) : mi(n) {}
    mutable int mi;
    int f(int n) { return mi + n; }
};

struct D : public S
{
    D(int n) : S(n) {}
};

int main()
{
    int S::* pmi = &S::mi;
    int (S::* pf)(int) = &S::f;

    const S s(7);
//  s.*pmi = 10; // error: cannot modify through mutable
    std::cout << s.*pmi << '\n';

    D d(7); // base pointers work with derived object
    D* pd = &d;
    std::cout << (d.*pf)(7) << ' '
              << (pd->*pf)(8) << '\n';
}

출력:

7
14 15

const S s(7)에서 s.*pmi를 읽는 건 mutable 멤버라 가능하지만, s.*pmi = 10처럼 쓰면 const 객체에서 수정하려는 셈이라 오류예요. 그리고 기반 클래스의 멤버 포인터가 파생 객체 d에도 잘 동작해요 ((d.*pf)(7)14).

표준 라이브러리

첨자 연산자는 많은 표준 컨테이너 클래스에서 오버로드돼요:

  • std::bitset — 특정 비트 접근
  • std::unique_ptr — 관리하는 배열의 인덱스 접근
  • std::basic_string — 지정 문자 접근
  • std::array, std::deque, std::vector — 지정 요소 접근
  • std::map, std::unordered_map — 지정 요소 접근 또는 삽입
  • std::reverse_iterator, std::move_iterator — 인덱스로 요소 접근
  • std::valarray — 요소·슬라이스·마스크 get/set
  • std::match_results — 지정 부분 일치(sub-match) 반환

역참조·멤버 연산자는 많은 반복자와 스마트 포인터 클래스에서 오버로드돼요:

  • std::unique_ptr, std::shared_ptr, std::auto_ptr — 관리하는 객체 역참조·멤버 접근
  • std::raw_storage_iterator, std::reverse_iterator, std::move_iterator, std::istream_iterator, std::istreambuf_iterator, std::regex_iterator, std::regex_token_iterator — 반복자 역참조·멤버 접근
  • std::back_insert_iterator, std::front_insert_iterator, std::insert_iterator, std::ostream_iterator, std::ostreambuf_iteratoroperator*는 no-op

표준 라이브러리 클래스 중 operator&를 오버로드하는 것은 없어요. 오버로드된 operator&의 가장 잘 알려진 예는 Microsoft COM 클래스 CComPtr인데, boost.spirit 같은 EDSL에도 나타날 수 있어요.

표준 라이브러리 클래스 중 operator->*를 오버로드하는 것도 없어요. 스마트 포인터 인터페이스의 일부가 될 수 있다는 제안이 있었고, 실제로 boost.phoenix의 actors가 그런 용도로 쓰지만, cpp.react 같은 EDSL에서 더 흔해요.

참고 (Notes)

기능 테스트 매크로 표준 기능
__cpp_multidimensional_subscript 202110L (C++23) 다차원 첨자 연산자

결함 보고 (Defect reports)

다음은 이전에 발행된 C++ 표준에 소급 적용된, 동작을 바꾸는 결함 보고예요.

DR 적용 대상 발행 당시 동작 올바른 동작
CWG 1213 C++11 배열 rvalue에 첨자 연산을 하면 lvalue가 됐음 xvalue로 재분류
CWG 1458 C++98 operator&를 선언하는 불완전 클래스 타입의 lvalue에 &를 적용하면 정의되지 않은 동작이었음 어떤 &를 쓰는지 불특정
CWG 1642 C++98 내장 멤버 포인터 접근 연산자의 rhs가 lvalue일 수 있었음 rvalue만 가능
CWG 1800 C++98 익명 유니온 멤버 유니온의 비정적 데이터 멤버에 &를 적용할 때 익명 유니온이 결과 타입에 포함되는지 불명확했음 익명 유니온은 결과 타입에 포함되지 않음
CWG 2614 C++98 E2가 참조 멤버나 열거자일 때 E1.E2의 결과가 불명확했음 명확히 함
CWG 2725 C++98 E2가 정적 멤버 함수일 때 E1.E2operator()의 왼쪽 피연산자가 아니어도 well-formed였음 이 경우 ill-formed
CWG 2748 C++98 E1이 null 포인터이고 E2가 정적 멤버를 가리킬 때 E1->E2의 동작이 불명확했음 이 경우 동작은 정의되지 않음
CWG 2813 C++98 E1.E2가 정적 멤버나 열거형을 가리키면 E1이 무시된 값 표현식이 아니었음 그렇다
CWG 2823 C++98 expr이 객체나 함수를 가리키지 않을 때 *expr의 동작이 불명확했음 명확히 함

더 알아보기 (Learn more)

  • 연산자 우선순위 — 각 연산자의 우선순위와 결합성을 정리한 표예요.
  • 연산자 오버로딩 — 사용자 정의 타입에서 연산자를 재정의하는 방법이에요.
  • 포인터 산술 — E1[E2]*(E1+E2)와 같음을 이해하려면 포인터 산술을 함께 보면 좋아요.