20 분 소요

모던 C++

복사 대입 연산자

복사 대입 연산자= 와 같은 기본 복사 대입 연산자와 산술 연산이나 비트 연산의 결과값을 대입하는 산술형 대입 연산자가 있습니다.(a += ba = a + b 와 결과가 같습니다.)

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
복사 대입 a = b O T& T::operator =(const T2& b); X
추가 대입 a += b O T& T::operator +=(const T2& b); T& operator +=(T& a, const T2& b);
빼기 대입 a -= b O T& T::operator -=(const T2& b); T& operator -=(T& a, const T2& b);
곱셈 대입 a *= b O T& T::operator *=(const T2& b); T& operator *=(T& a, const T2& b);
나누기 대입 a /= b O T& T::operator /=(const T2& b); T& operator /=(T& a, const T2& b);
나머지 대입 a %= b O T& T::operator %=(const T2& b); T& operator %=(T& a, const T2& b);
비트 AND 대입 a &= b O T& T::operator &=(const T2& b); T& operator &=(T& a, const T2& b);
비트 OR 대입 a |= b O T& T::operator |=(const T2& b); T& operator |=(T& a, const T2& b);
비트 XOR 대입 a ^= b O T& T::operator ^=(const T2& b); T& operator ^=(T& a, const T2& b);
비트 Left Shift 대입 a <<= b O T& T::operator <<=(const T2& b); T& operator <<=(T& a, const T2& b);
비트 Right Shift 대입 a >>= b O T& T::operator >>=(const T2& b); T& operator >>=(T& a, const T2& b);

산술 연산자

산술 연산의 결과를 계산합니다.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
단항 양수 +a O T T::operator +() const; T operator +(const T &a);
단항 음수 -a O T T::operator -() const; T operator -(const T &a);
더하기 a + b O T T::operator +(const T2 &b) const; T operator +(const T &a, const T2 &b);
빼기 a - b O T T::operator -(const T2 &b) const; T operator -(const T &a, const T2 &b);
곱하기 a * b O T T::operator *(const T2 &b) const; T operator *(const T &a, const T2 &b);
나누기 a / b O T T::operator /(const T2 &b) const; T operator /(const T &a, const T2 &b);
나머지 a % b O T T::operator %(const T2 &b) const; T operator %(const T &a, const T2 &b);
비트 NOT ~a O T T::operator ~() const; T operator ~(const T &a);
비트 AND a & b O T T::operator &(const T2 &b) const; T operator &(const T &a, const T2 &b);
비트 OR a | b O T T::operator |(const T2 &b) const; T operator |(const T &a, const T2 &b);
비트 XOR a ^ b O T T::operator ^(const T2 &b) const; T operator ^(const T &a, const T2 &b);
비트 Left Shift a << b O T T::operator <<(const T2 &b) const; T operator <<(const T &a, const T2 &b);
비트 Right Shift a >> b O T T::operator >>(const T2 &b) const; T operator >>(const T &a, const T2 &b);

비트 쉬프트 연산자

<<은 비트를 왼쪽으로 이동시킵니다. 왼쪽으로 이동하면서 비워지는 비트는 표준에 정의되진 않았으나 0으로 채워집니다.(2를 곱하는 것과 동일합니다.)

1
2
3
4
5
6
7
8
9
10
11
char ch = 0x20; // 0010 0000는 십진수 32
EXPECT_TRUE(ch == 32);

ch = ch << 1; // 0100 0000는 십진수 64. << 1은 곱하기 2의 효과
EXPECT_TRUE(ch == 64);

ch = ch << 1; // 1000 0000은 음수 십진수 -128.
EXPECT_TRUE(ch == -128);

ch = ch << 1; // 0000 0000은 0. 부호 비트를 이동시키면 결과가 정의되지 않았으나, 대부분 0으로 채웁니다.
EXPECT_TRUE(ch == 0);

>>은 비트를 오른쪽으로 이동시킵니다. 오른쪽으로 이동하면서 비워지는 비트는 표준에 정의되진 않았으나 양수이면 0으로 채워지고, 음수이면 1로 채워집니다.(2를 나누는 것과 동일합니다.)

1
2
3
4
5
6
7
8
9
10
11
12
char ch = 0x20; // 0010 0000는 십진수 32 
EXPECT_TRUE(ch == 32);

ch = ch >> 1; // 0001 0000는 십진수 16. >> 1은 나누기 2의 효과
EXPECT_TRUE(ch == 16);

ch = 0x80; // 1000 0000 최상위 비트가 1인 음수
EXPECT_TRUE(ch == -128);

ch = ch >> 1; // 1100 0000는 십진수 -64
std::cout << (int)ch << std::endl;
EXPECT_TRUE(ch == -64);

(C++20~) 비트 쉬프트 연산자의 기본 비트가 표준화되어 << 1는 곱하기 2의 효과가 있는 비트(즉, 0)로 채워지고, >> 1은 나누기 2의 효과가 있는 비트(즉, 양수면 0, 음수면 1)로 채워집니다.

증감 연산자

전위형 연산의 경우 값을 먼저 증감시킨뒤 증감한 값을 리턴합니다만, 후위형 연산의 경우는 값을 증감 시키기 전의 값을 리턴합니다.

후위형의 이러한 특징은 불필요한 복사 부하가 생길 뿐만아니라, 분석을 헷갈리게 할 수 있기 때문에(의도인지, 실수인지), 후위형 보다는 전위형 증감 연산자를 사용하는게 좋습니다.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
전위 증가 ++a O T& T::operator ++(); T& operator ++(T& a);
전위 감소 --a O T& T::operator --(); T& operator --(T& a);
후위 증가 a++ O const T T::operator ++(int); const T operator ++(T& a, int);
후위 감소 a-- O const T T::operator --(int); const T operator --(T& a, int);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 전위형 - 증감시킨 값을 리턴
{
    int n = 10;
    int val = ++n;
    EXPECT_TRUE(val == 11); // 증가시킨 후 값
    EXPECT_TRUE(n == 11);
}
// 후위형 - 증감시키기 전의 값을 리턴
{
    int n = 10;
    int val = n++; 
    EXPECT_TRUE(val == 10); // (△) 비권장. 의도한 것인지 조금 헷갈립니다. 증가시킨 전 값. 
    EXPECT_TRUE(n == 11);
}

논리 연산자

NOT, AND, OR 논리 조건에 맞춰 true, false를 평가합니다.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
NOT !a O bool T::operator !() const; bool operator !(const T &a);
AND a && b O bool T::operator &&(const T2 &b) const; bool operator &&(const T &a, const T2 &b);
OR a || b O bool T::operator ||(const T2 &b) const; bool operator ||(const T &a, const T2 &b);

비교 연산자

대소 비교를 합니다. 실수 비교 연산의 경우는 오차 범위를 고려해야 합니다.(실수 비교 참고)

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
같다 a == b O bool T::operator ==(const T2& b) const; bool operator ==(const T& a, const T2& b);
같지 않다. a != b O bool T::operator !=(const T2& b) const; bool operator !=(const T& a, const T2& b);
미만 a < b O bool T::operator <(const T2& b) const; bool operator <(const T& a, const T2& b);
a > b O bool T::operator >(const T2& b) const; bool operator >(const T& a, const T2& b);
이하 a <= b O bool T::operator <=(const T2& b) const; bool operator <=(const T& a, const T2& b);
이상 a >= b O bool T::operator >=(const T2& b) const; bool operator >=(const T& a, const T2& b);

(C++20~) 삼중 비교 연산자가 추가되어 비교 연산자 구현이 간소화 되었습니다.

대소 비교의 논리 조건

대소 비교는 하기 논리 조건을 만족해야 합니다.

  1. x < x 는 거짓
  2. x < y 이고 y < z 이면, x < z
  3. !(x < y) 이면, (y <= x)
  4. x < y 도 아니고 y < x 도 아니면, xy 와 동등함(equivalence. 동등관계)
  5. x equivalence y 이고 y equivalence z 이면, x equivalence z

상기 논리 조건이 모두 만족한다면(만족해야만 합니다.), 비교 연산들은 모두 < 로 표현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int x = 10;
int y = 10;
EXPECT_TRUE(!(x < y || y < x)); // x == y

x = 10;
y = 20;   
EXPECT_TRUE((x < y || y < x)); // x != y

x = 10;
y = 20;
EXPECT_TRUE((x < y)); // x < y

x = 20;
y = 10;
EXPECT_TRUE((y < x)); // x > y      

x = 10;
y = 20;
EXPECT_TRUE(!(y < x)); // x <= y

x = 10;
y = 10;
EXPECT_TRUE(!(y < x)); // x <= y   
x = 20;
y = 10;
EXPECT_TRUE(!(x < y)); // x >= y

x = 10;
y = 10;
EXPECT_TRUE(!(x < y)); // x >= y 

형변환 연산자

형변환은 최대한 안하는 것이 좋습니다. 형변환의 자세한 내용은 형변환을 참고하세요.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
C언어 스타일 (T)a O operator T() const; X
상수성만 변환 const_cast X X X
타입 유사성을 지키며 변환 static_cast X X X
타입 유사성을 지키며 변환. (Runtime Type Info(RTTI) 지원) dynamic_cast X X X
상속관계를 무시하고 변환 reinterpret_cast X X X

접근 연산자

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
배열 요소 a[b] O R& T::operator [](S b);
const R& T::operator [](S b) const;
X
포인터 실제값 *a O R& T::operator *();
const R& T::operator *() const;
R& operator *(T a);
개체 주소 &a O R* T::operator &();
const R* T::operator &() const;
R* operator &(T a);
개체의 멤버 a.b X X X
포인터 개체의 멤버 a->b O R* T::operator ->();
const R* T::operator ->() const;
X
개체 멤버에 대한 포인터 a.*b X X X
포인터인 개체 멤버에 대한 포인터 a->*b O R& T::operator ->*(S b);
const R& T::operator ->*(S b) const;
R& operator ->*(T a, S b);
범위 확인 연산자(네임스페이스 멤버) namespaceName::member X X X
범위 확인 연산자(전역 멤버) ::member X X X
범위 확인 연산자(개체의 정적 멤버, 열거형) className::member X X X
범위 확인 연산자(개체 멤버) className::member X X X

함수 호출 연산자

괄호 표현식으로 사용할 수 있는 특수한 표현식입니다. 표준 템플릿 라이브러리(Standard Template Library, STL)의 함수자 처럼 사용할 수 있습니다.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
함수 호출 연산자 a(a1, a2) O R T::operator ()(Param1 &a1, Param2 &a2, ...); X
1
2
3
4
5
6
7
8
class T {
public:
    int operator ()(int a, int b) const {return a + b;}
};

T t;
EXPECT_TRUE(t(10, 20) == 30); // operator() 호출
EXPECT_TRUE(t.operator ()(10, 20) == 30); // t(10, 20) 호출과 동일. operator()를 명시적으로 호출        

콤마 연산자

a표현식을 평가하고, b표현식을 평가합니다. (a, b, c, d와 같이 나열할 수 있습니다.) 코드 분석이 어려워 권장하지 않습니다.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
콤마 연산자 a, b O T2& T::operator ,(T2 &b); T2& operator ,(const T &a, T2 &b);
1
2
3
4
5
6
7
8
9
10
11
class T {
public:
    static int f(int val) {return val;}
};

int arr[] = {1, 2, 3};
int i = 0;

// 1를 증가시키고, arr의 i 항목을 f 에 전달합니다.
// (△) 비권장. 분석하기 복잡합니다.
EXPECT_TRUE(T::f((++i, arr[i])) == 2);  

조건 연산자

a가 참인 경우 b, 거짓인 경우 c를 평가합니다.

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
조건 연산자 a ? b : c X X X
1
2
int result = true ? 10 : 20;
EXPECT_TRUE(result == 10);

생성/소멸 연산자

개체 생성시 사용하는 new는 하기 단계를 수행합니다.

  1. 전역 operator new(std::size_t)로 메모리 공간 할당
  2. 구조체이거나 클래스이면 위치 지정 생성(Placement New)operator new(size_t, void*)를 실행하여 생성자 호출
  3. 메모리 주소를 해당 타입으로 형변환하여 리턴

new

로 구성되어 있습니다. 좀더 자세한 내용은 개체 생성과 소멸을 참고하세요.

항목 내용 사용예 연산자 오버로딩 개체 멤버 정의 전역 정의
operator new(std::size_t) 개체 생성 T* p = new T; O void* operator new(std::size_t sz); void* operator new(std::size_t sz);
operator delete(void*) 개체 소멸 delete p; O void operator delete(void* ptr, std::size_t sz) void operator delete(void* ptr, std::size_t sz)
operator new[](std::size_t) 배열 생성시 사용 T* arr = new T[10]; O void* operator new[](std::size_t sz); void* operator new[](std::size_t sz);
operator delete[](void*) 배열 소멸시 사용 delete[] arr; O void operator delete[](void* ptr); void operator delete[](void* ptr);
operator new(size_t, void*) 위치 지정 생성(Placement New). 특정 메모리 위치에 개체 생성자 호출 T* p = new(buf) T; X X X

sizeof 연산자

개체의 용량을 리턴합니다. 단 참조자의 경우 참조하는 개체와 동일 크기를 리턴하도록 스펙에 정의되어 있습니다.(sizeof(T&) == sizeof(T), 기본 타입 언급)

항목 내용
sizeof(개체명) 개체의 용량 리턴
sizeof(배열명) 배열의 전체 용량 리턴
sizeof(타입명) 타입이나 클래스명, 구조체명, 공용체명의 용량 리턴
sizeof(참조자) 참조하는 개체와 동일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
short i;
int arr[10];
class T {
    int m_Val;
};
struct S {
    int x;
    int y;
};
S s;
S& ref = s; // ref는 s의 참조자

EXPECT_TRUE(sizeof(i) == 2); // short는 2byte
EXPECT_TRUE(sizeof(arr) == sizeof(int) * 10); // 배열은 요소의 전체 크기
EXPECT_TRUE(sizeof(T) = sizeof(int) * 1); // 클래스와 구조체는 멤버 변수들의 합 
EXPECT_TRUE(sizeof(S) = sizeof(int) * 2); 
EXPECT_TRUE(sizeof(s) == sizeof(ref)); // 참조자의 크기는 참조하는 개체의 크기와 동일

(C++11~) sizeof…() 연산자가 추가되어 가변 템플릿에서 파라메터 팩의 인자수를 구할 수 있습니다.
(C++11~) 멤버의 sizeof()시 동작이 개선되어 개체를 인스턴스화 하지 않더라도 개체 멤버의 크기를 구할 수 있습니다.

typeid 연산자

개체의 타입 비교를 위해 사용합니다.(<typeinfo>#include해야 합니다.)

항목 내용
typeid(개체명) 개체 type_info 리턴
typeid(타입명) 타입이나 클래스명, 구조체명, 공용체명의 type_info 리턴

가상 함수가 있는 개체인지 아닌지에 따라 동작이 다릅니다.

항목 내용
가상 함수가 없는 경우 정의한 개체 타입을 리턴합니다.
가상 함수가 있는 경우 참조하는 개체 타입을 리턴합니다.

type_infoname()은 타입의 이름입니다만, 모든 타입에 대해 유일하다고 보장하지는 않습니다.(컴파일러 제조사 마음입니다.) 따라서, 개발이나 디버깅시의 힌트를 참고하는 정도로만 사용하시기 바랍니다.

다음은 thype_info의 멤버 함수입니다.

항목 내용
== 타입이 같은지 검사합니다.
before() 어느 타입이 먼저 구현 정의되었는지 검사합니다. 타입의 대소 비교시 사용할 수 있습니다.
name() 타입의 이름입니다만, 모든 타입에 대해 유일하다고 보장하지는 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 가상 함수 없음
class Base1 {};
class Derived1 : public Base1 {};

// 가상 함수 있음
class Base2 { 
public:
    virtual void f() {}
};
class Derived2 : public Base2 {};

// Base1과 개체 b1은 이름이 동일합니다.
{
    Base1 b1;
    const std::type_info& ti1 = typeid(Base1);
    const std::type_info& ti2 = typeid(b1);
    EXPECT_TRUE(ti1 == ti2);
    EXPECT_TRUE(ti1.name() == ti2.name());
}
// 가상 함수가 없는 경우의 참조 - 참조 대상이 Derived1이지만, 정의한 Base1 타입으로 변경됨
{
    Derived1 d1;
    Base1& b1Ref = d1; // 가상 함수 없음

    // b1Ref = d1으로 bRef는 Base1 타입이 됨
    EXPECT_TRUE(typeid(b1Ref).name() == typeid(Base1).name());
}
// 가상 함수가 있는 경우의 참조 - 참조 대상인 Derived2 타입으로 유지됨
{
    Derived2 d2;
    Base2& b2Ref = d2; // 가상 함수 있음

    // b2Ref = d2로 b2Ref는 다형적 동작하며, 여전히 Derived2 타입임.(원래 개체의 타입 정보)
    EXPECT_TRUE(typeid(b2Ref).name() == typeid(Derived2).name());   
}  
// before()를 이용한 대소 비교
{
    Derived2 d1, d2;
    Base2& b1 = d1;
    Base2& b2 = d2;

    const std::type_info& ti1 = typeid(b1);
    const std::type_info& ti2 = typeid(b2);
    EXPECT_TRUE(ti1.before(ti2) == false); // 모두 Derived2여서 ti1 < ti2는 false입니다. 
    EXPECT_TRUE(ti2.before(ti1) == false); // 모두 Derived2여서 ti2 > ti1는 false입니다.          
}

스트림 연산자

항목 내용 연산자 오버로딩 개체 멤버 정의 개체 비멤버 정의
출력 cout << a; O X ostream& operator <<(ostream& os, const T& a)
입력 cin >> a; O X istream& operator >>(istream& is, T& a)

연산자 우선 순위

기본적으로 평가되는 연산자 우선 순위는 하기와 같으며, ()로 감싸서 우선 순위를 높일 수 있습니다.

순위 연산자
1 ::(범위 확인 연산자)
2 a++, a--, ()(함수 호출 연산자), a[], ., ->
3 ++a, --a, +a, -a, !, ~, (int)(C언어 스타일 형변환), a, &a, sizeof, new, new[], delete, delete[]
4 .*, ->*
5 a * b, a / b, a %b
6 a + b, a - b
7 <<, >>
8 <, <==, >, >==
9 ==, !=
10 a & b
11 a ^ b
12 a | b
13 a && b
14 a || b
15 a ? b : c, =, +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=
16 ,

연산자 오버로딩

클래스나 구조체에서 캡슐화를 위해 연산자를 오버로딩할 수 있습니다.

복사 대입 연산자

하기는 복사 대입 연산자(=) 연산자를 오버로딩한 예입니다. T& 와 같이 자기 자신의 참조를 리턴하는데요, 이는 t1 = t2 = t2;과 같이 연달아 대입하는 경우를 지원하고, 복사 부하를 줄이기 위함입니다.(예외에 안전할 수 있도록 보증하는 방법은 swap을 이용한 예외 보증 복사 대입 연산자를 참고하세요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
class T {
private:
    int m_X;
    int m_Y;
public:
    T& operator =(const T& other) {
        this->m_X = other.m_X;
        this->m_Y = other.m_Y;
        return *this; // 자기 자신을 리턴합니다.
    }
}; 
T t1, t2, t3;
t1 = t2 = t3; // t2 = t3의 결과 t2의 참조자를 리턴하고, t1에 대입합니다.

+= 연산자

하기는 += 연산자를 오버로딩한 예입니다. int형도 지원하기 위해 T& operator +=(int val) 도 구현 하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class T {
private:
    int m_Val;
public:
    explicit T(int val) : m_Val(val) {}
    int GetVal() const {return m_Val;}

    // 같은 T타입인 경우
    T& operator +=(const T& other) {
        this->m_Val += other.m_Val;
        return *this; // 자기 자신을 리턴합니다.
    }

    // int 형도 지원
    T& operator +=(int val) {
        this->m_Val += val;
        return *this; // 자기 자신을 리턴합니다.        
    }
};

T t1(10), t2(20);
t1 += t2; // operator +=(const T& other) 호출
EXPECT_TRUE(t1.GetVal() == 30); 

t1 += 10;// operator +=(int val) 호출
EXPECT_TRUE(t1.GetVal() == 40); 

+ 연산자

하기는 이항 연산자인 +연산자를 오버로딩한 예입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class T {
private:
    int m_Val;
public:
    explicit T(int val) : m_Val(val) {}
    int GetVal() const {return m_Val;}

    // 같은 T타입인 경우
    T operator +(const T& other) {
        return T(this->m_Val + other.m_Val); // 새로운 개체를 리턴합니다.
    }

    // int 형도 지원
    T operator +(int val) {
        return T(this->m_Val + val); // 새로운 개체를 리턴합니다.       
    }
};

T t1(10), t2(20);
T t3(0);
t3 = t1 + t2; // operator +(const T& other) 호출
EXPECT_TRUE(t1.GetVal() == 10); 
EXPECT_TRUE(t3.GetVal() == 30); 

t3 = t1 + 10;// operator +(int val) 호출
EXPECT_TRUE(t1.GetVal() == 10); 
EXPECT_TRUE(t3.GetVal() == 20); 

+ 보다는 +=이 좋은 이유

산술형 대입 연산자가 참조자 형식(T&)를 리턴하는데 반해, 이항 산술 연산의 경우 개체 형식(T, 값 타입)을 리턴하는데요, 이는 연산의 결과값을 저장할 임시 개체가 있어야 하기 때문입니다.

따라서, 임시 개체 생성 부하가 없도록 산술형 대입 연산자를 사용하는 코딩 습관을 가지시는게 좋습니다.

1
2
3
4
5
6
7
8
9
T t1(10);
T t2(20);

// (△) 비권장. 임시 개체가 생성됨
T t3 = t1 + t2; // t1 + t2 인 임시 개체를 생성. t3의 복사 생성자 호출하여 임시 개체 대입

// (O) 권장. 임시 개체가 생성되지 않음
T t3 = t1; // t3의 복사 생성자를 호출하여 t1값 대입
t3 += t2; // t3에 t2값 증가

+ 연산자 비멤버 버전

+연산시 int형을 먼저 작성하면 컴파일 되지 않습니다. t3 = 10 + t1; 은 사실은 10.operator +(T other) 호출하는 것과 같기 때문입니다. int형인 10에는 해당 함수가 없으므로 컴파일 오류가 납니다.

1
2
3
4
5
6
7
8
9
T t1(10);
T t2(0);
T t3(0);

t2 = t1 + 10;// t1.operator +(int val) 호출
t2 = t1.operator +(10); // t2 = t1 + 10과 동일

t3 = 10 + t1;// (X) 컴파일 오류. 10.operator +(T other) 호출합니다. int 형인 10에는 해당 operator 가 없습니다.
EXPECT_TRUE(t2.GetVal() == 20 && t3.GetVal() == 20);

이를 허용하려면 비멤버 버전을 만들면 됩니다.

1
2
3
4
5
// T t = 10 + other; 지원
inline T operator +(int left, const T& right) {
    T result(left + right.GetVal());
    return result;
}

증감 연산자

증감 연산자는 연산자 오버로딩시 전위형과 후위형을 구분하기 위해, 후위형의 경우 인자int를 더미(Dummy)로 넣습니다.

또한 후위형은

  1. 증감 시키기 전의 값을 리턴하기 위해 현재값을 복제하며(후위형은 분석을 헷갈리게 할 뿐 아니라, 복사 부하 까지 있습니다. 이러한 특성 때문에 후위형 보다는 전위형을 쓰시는게 좋습니다.),
  2. t++++;처럼 사용할 수 없도록 const T를 리턴합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class T {
private:
    int m_Val;
public:
    explicit T(int val) : m_Val(val) {}
    int GetVal() const {return m_Val;}

    // 전위형
    T& operator ++() {
        ++m_Val;
        return *this; // 자기 자신을 리턴합니다.
    }
    // 후위형. 인자 int는 전위형과 구분하기 위한 더미(Dummy)입니다.
    const T operator ++(int) { // t++++가 안되도록 const T를 리턴합니다.
        T result = *this; // 복제합니다.
        ++m_Val; // this의 값을 증가시킵니다.
        return result; // 증가시키기 전에 복제한 값을 리턴합니다.
    }
};

T t(10);
T t1 = ++t;
EXPECT_TRUE(t1.GetVal() == 11); // 증가시킨 후 값
EXPECT_TRUE(t.GetVal() == 11);

T t2 = t++; 
EXPECT_TRUE(t2.GetVal() == 11); // (△) 비권장. 의도한 것인지 조금 헷갈립니다. 증가시킨 전 값. 
EXPECT_TRUE(t.GetVal() == 12);

비교 연산자

비교 연산자는 일관된 논리 결과값을 구하기 위해 <==만 구현하고, !=, <, >, <=, >===<로부터 구현하는게 좋습니다.(대소 비교의 논리 조건 참고)

각각 구현하다보면, a < bb < c 인데, c < a 가 되버리는 논리적 결함이 생길 수도 있거든요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class T {
private:
    int m_Val;
public:
    explicit T(int val) : m_Val(val) {}
    int GetVal() const {return m_Val;}

    bool operator <(const T& other) const {
        return this->m_Val < other.m_Val;
    } 

    bool operator ==(const T& other) const {
        return m_Val == other.m_Val; // < 로부터 구현하면, !(*this < other || other < *this)로 할 수 있습니다. 단 < 을 2회 하므로 비효율적입니다.
    }
    bool operator !=(const T& other) const {
        return !(*this == other);
    }
    bool operator >(const T& other) const {
        return other < *this;
    }
    bool operator <=(const T& other) const {
        return !(other < *this);
    }
    bool operator >=(const T& other) const {
        return !(*this < other);
    }
};

T t1(10);
T t2(10);
EXPECT_TRUE(t1 == t2);

T t1(10);
T t2(20);   
EXPECT_TRUE(t1 != t2);

T t1(10);
T t2(20);  
EXPECT_TRUE(t1 < t2); 

T t1(20);
T t2(10);  
EXPECT_TRUE(t1 > t2);  

T t1(10);
T t2(20); 
EXPECT_TRUE(t1 <= t2);

T t1(10);
T t2(10);
EXPECT_TRUE(t1 <= t2);

T t1(20);
T t2(10); 
EXPECT_TRUE(t1 >= t2);

T t1(10);
T t2(10); 
EXPECT_TRUE(t1 >= t2);   

비교 연산은 truefalse던, 결과값이 판단되면 재빠르게 리턴하는게 좋습니다. 특히, 멤버 변수가 많은 경우 연산을 많이 하게 되니 다음과 같이 작성하는게 좋습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class T {
private:
    int m_X;
    int m_Y;
    int m_Z;

public:  

    bool operator <(const T& other) const {
        if (this->m_X < other.m_X) return true; // 작다고 판단되면 바로 리턴
        if (other.m_X < this->m_X) return false; // 크다고 판단되면 바로 리턴
  
        if (this->m_Y < other.m_Y) return true;
        if (other.m_Y < this->m_Y) return false;

        return this->m_Z < other.m_Z;
    }
    bool operator ==(const T& other) const { 
        if (this->m_X != other.m_X) return false; // 다르다고 판단되면 바로 리턴
        if (this->m_Y != other.m_Y) return false;

        return this->m_Z == other.m_Z;
    }
    bool operator !=(const T& other) const {
        return !(*this == other);
    }
    bool operator >(const T& other) const {
        return other < *this;
    }
    bool operator <=(const T& other) const {
        return !(other < *this);
    }
    bool operator >=(const T& other) const {
        return !(*this < other);
    }
};

(C++20~) 삼중 비교 연산자가 추가되어 비교 연산자 구현이 간소화 되었습니다.

열거형 연산자 오버로딩

열거형연산자 오버로딩을 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Week {
public:
    enum Val {
        Sunday, Monday, Tuesday, Wednesday, 
        Thursday, Friday, Saturday
    };
};
// 전위 증가. 
Week::Val& operator ++(Week::Val& d) { 
    return d = (Week::Saturday == d) ? Week::Sunday : static_cast<Week::Val>(d + 1);
}  

Week::Val val = Week::Saturday;

EXPECT_TRUE(++val == Week::Sunday); // 토요일 에서 1 증가하면, 제일 처음 값인 일요일로 순회됨
EXPECT_TRUE(++val == Week::Monday); // 일요일 에서 1 증가하여 월요일

댓글남기기