I'm FanJae.

[C++] Operator Overloading II 본문

C++/Basic

[C++] Operator Overloading II

FanJae 2024. 8. 24. 21:35

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. 증가(++) 연산자(Increment Operator)

#include <iostream>

class Point
{
public:
     int x{0};
     int y{0};
     Point() = default;
     Point(int x, int y) : x{x}, y{y} { }
};

int main()
{
    int n = 3;
    ++n;
    
    Point p{1,1};
    ++p;
}

- 일반적으로 객체를 ++한다는 것이 조금 이상해 보일 수 있지만, 학습을 위해서 해보고자 한다.

- 증가(++) / 감소(--) 연산자 재정의가 사용되는 예는 C++ 표준 라이브러리인 STL의 반복자가 사용한다.

 

1-1. 증감 연산자 재정의 구현

#include <iostream>

class Point
{
public:
     int x{0};
     int y{0};
     Point() = default;
     Point(int x, int y) : x{x}, y{y} { }
     
     Point operator++()
     {
          std::cout << "prefix ++" << std::endl;
          ++x; ++y;
          return *this;
     }
     
     Point operator++(int)
     {
          std::cout << "postfix ++";
          Point temp = *this; // 증가하기 이전 값 보관
          ++x; ++y;
          return temp;
     }
};

int main()
{ 
    Point p{1,1};
    ++p; // p.operator++() // p.operator++()
    p++; // p.operator++() // p.operator++(int)
}

- 증감 연산자는 전위형과 후위형을 구분해야한다.

- 두개는 약간 의미가 다르기 때문이다. 후위형을 만들 때는 함수 인자를 int 타입을 한개 가져야 한다

- 위 방법은 아래와 같은 2가지를 개선할 수 있다.

 

① 전위형은 ++++p 표현식이 가능해야 한다.

#include <iostream>

class Point
{
public:
     int x{0};
     int y{0};
     Point() = default;
     Point(int x, int y) : x{x}, y{y} { }
     
     Point operator++()
     {
          ++x; ++y;
          return *this;
     }
     
     Point operator++(int)
     {
          Point temp = *this; // 증가하기 이전 값 보관
          ++x; ++y;
          return temp;
     }
};

int main()
{ 
    Point p{1,1};
    
    ++++p; // ++(++p) (p.operator++()).operator++()
    std::cout << p.x << "," << p.y << std::endl;
}

- ++++p; 라고 했으면 증감 연산자를 두번 호출한 것이기 때문에 원래 기대값은 3이지만, 2가 나온다.

- 그 이유는 Point 값을 리턴하는 것에 있다.

- 전위형은 ++++p 표현식이 가능해야 한다.

- 즉, 자기 자신을 참조로 반환할 수 있어야 한다.

Point& operator++()
{
    ++x; ++y;
    return *this;
}

 

② 후위형은 전위형을 사용해서 구현한다.

Point operator++(int)
{
    Point temp = *this;
    //++x; ++y;
    ++(*this); // this->operator++()
    return temp;
}

- 이와 같이 구현하면 후위형 또한, 전위형을 호출하게 된다..

-  이렇게 되면 전위형 쪽에서 연산이 바뀌어도 후위형도 함께 바뀌게 된다. 

 

전위형이 후위형 보다 빠르다. 반환값을 사용하지 않고 단순히 증가만 한다면, 전위형을 사용하는 것이 좋다. (과거)

※ 요즘 컴파일러는 코드의 문맥을 파악해서 동일한 기계어 코드를 생성한다.


2. 대입(=) 연산자 (Assignment Operator)

int main()
{
    Point p1{1,2};
    Point p2;
    
    Point p3 = p1;
    
    p2 = p1;
    
    std::cout << p2.x << "," << p2.y << std::endl;
}

 

2-1. 복사 생성자 vs 대입 연산자

복사 생성자 객체를 생성할 때 초기화 하는 것
Point p3(p1);
Point p3 = p1;
대입연산자 객체를 생성 후에 값을 넣는 것
p2 = p1; // p2.operator=(p1)

- 대입 연산자에 대해서는 사용자가 만들지 않아도 컴파일러가 기본 구현을 제공한다.

- 이 경우, 모든 멤버를 복사한다. 

Point& operator=(const Point& other)
{
    x = other.x;
    y = other.y;

    return *this;
}

 

2-2. 대입 연산자의 구현

- 대입 연산자는 멤버 함수로만 만들 수 있다.

#include <iostream>
class Point
{
public:
	int x{0};
	int y{0};
	Point() = default;
	Point(int x, int y) : x{x}, y{y} {}	
    
    void operator=(const Point& other) 
    {
    	x = other.y;
        y = other.x;
    }
};

int main()
{
	Point p1{1,2};
	Point p2;

	Point p3 = p1;
	
	p2 = p1;

	std::cout << p2.x << "," << p2.y << std::endl;
}

- 잘 들어가는지 확인하기 위해 역순으로 넣었다.

- 하지만 이 코드도 문제가 존재한다. 예를들어, 아래와 같은 상황이다.

(n = 10) = 20; // C++에서 이런 문법이 가능해야한다.
(p2 = p1) = p1; // 현재 코드는 void를 리턴하기 때문에 불가능하다. 즉, 참조로 리턴한다.

 

- 이처럼, void를 리턴하면 대입할 수 없는 상황이 발생하기 때문에, 문제다.

- 일반적으로 C++ 측에서 지향하는 것이 '사용자 정의 타입도 int와 동일하게 동작하도록 한다'이다.

- 따라서, 자신을 참조로 반환한다.

 

개선점 1. 자신을 참조로 반환해야 한다.

#include <iostream>
class Point
{
public:
	int x{0};
	int y{0};
	Point() = default;
	Point(int x, int y) : x{x}, y{y} {}	
    
    Point& operator=(const Point& other) 
    {
    	x = other.y;
        y = other.x;
        
        return *this;
    }
};

int main()
{
	Point p1{1,2};
	Point p2;

	Point p3 = p1;
	
	p2 = p1;

	std::cout << p2.x << "," << p2.y << std::endl;
}

 

② 개선점 2. 자신과의 대입을 조사한다.

p2 = p2;
Point& operator=(const Point& other) 
{
    if (&other == this) return *this; 
        
    x = other.y;
    y = other.x;
        
    return *this;
};

- 자신을 다시 대입을 하는데 굳이 다시 값을 넣어줄 필요는 없다. 이를 조사하여 바로 리턴하도록 처리한다.

 

2-3. 대입 연산자에 대한 구현을 알아야 하는 이유

- Point 같은 일반적인 경우는 컴파일러 제공버전의 사용도 무관하다.

- 하지만, 클래스 안에 포인터 멤버 등이 있으면 주의가 필요하다.

- 컴파일러가 대입 연산자를 만들지 못하게 하려면 함수 삭제(=delete) 문법을 사용한다.

#include <iostream>
class Point
{
public:
	int x{0};
	int y{0};
	Point() = default;
	Point(int x, int y) : x{x}, y{y} {}	
    
    Point& operator=(const Point& other) = delete;
};

int main()
{
    Point p1{1,2};
    Point p2;
    Point p3;
}

- 이와 같이 처리하면 컴파일러가 대입연산자를 만들지 않는다.

 

2-4. 요즘 새로 나오는 언어에서는 대입 연산자가 void를 반환하기도 한다.

int main()
{
    int n = 10;
    (n = 10) = 20; // n = 10의 결과가 다시 n으로 나온다.
    
    if ( n = 5 ) // 일반적으로 이를 의도하지 않고 했을 가능성이 크다.
    {
    
    }
}

- 대입 연산자의 결과가 void인 것이 좋을까?

- 일부 C++은 자신을 참조로 반환하는 것이 원칙이지만 요즘 나오는 많은 새로운 언어는 void를 반환하고 있다.


3. 변환 연산자(Conversion operator)

class TcpConnect
{
    int state{0};
public:
    void connect()
    {
         // IP 와 port 번호를 받아서
         // 서버에 접속하는 코드.
    }
};

int main()
{
    TcpConnect tcp;
    tcp.connect();
    
//  if ( tcp.is_connected() )
    if ( tcp )
    {
    
    }
}

- 객체의 유효성을 if 문으로 조사할 수 있을까?

- 객체가 bool로 변환이 가능해야한다.

- bool 뿐 아니라 if 조건식에 놓일 수 있는 타입으로 변환될 수 있으면 된다.

 

3-1. 변환 연산자란

- 객체가 다른 타입으로 변환이 필요할 때 호출되는 함수이다.

int n = tcp; tcp.operator int();
double d = tcp; tcp.operator double();
Point p = tcp; tcp.operator Point();

 

3-2. 변환 연산자의 구현

class TcpConnect
{
    int state{0};
public:
    void connect() { }
    
    operator bool() const
    {
        return state != 0;
    }
};

int main()
{
    TcpConnect tcp;
    tcp.connect();
    
    if ( tcp ) // tcp.operator bool() 
    {
    }
}

- 변환 연산자의 특징은 객체가 다른 타입으로 변환되어야 할 때 호출이 된다.

- 반환 타입을 표기 하지 않는다. 함수 이름 자체에 반환 타입이 포함되는게 특징이다.

 

3-3. 변환 연산자의 위험성

class TcpConnect
{
    int state{0};
public:
    void connect() { }
    
    operator bool() const
    {
        return state != 0;
    }
};

void foo(bool b) {}

int main()
{
    TcpConnect tcp;
    
    bool b1 = tcp;
    bool b2(tcp);
    bool b3 = static_cast<bool>(tcp);
    foo(tcp);
    
    tcp << 10; // bool은 정수이므로 이것도 가능하다.
    
    if ( tcp ) { }
}

- 객체의 변환은 의도하지 않은 side effect가 많다는 문제가 있다.

- 버그의 원인이 될 수 있다. 가급적 사용해야 한다면, explicit를 붙이는 것이 좋다.

foo(tcp); // 이게 의도일까?

tcp << 10; // 이게 과연 의도일까?

- 사실 이 2개는 개발자 의도라고 보기 매우 어렵다.

 

3-3-1. explicit 변환 연산자

- 직접 초기화와 명시적 변환만 허용

- 암시적 변환 허용 안됨

- if 조건식 안에서는 사용 가능

int main()
{
    TcpConnect tcp;
    
    bool b1 = tcp; // error
    bool b2(tcp); // ok
    bool b3 = static_cast<bool>(tcp); // ok
    foo(tcp); // error
    
    tcp << 10; // error
    
    if ( tcp ) { } // ok
}

 

Explicit 변환 연산자와 표현식

bool b1 = tcp; X
bool b2(tcp); O
b1 = static_cast<bool>(tcp); O
foo(tcp); X
tcp << 10; X
if ( tcp ) { } O

- Explicit가 아니라면 위 코드는 모두 허용된다.

 

 

② nullptr -> bool 변환

- Explicit operator bool()과 동일하게 작동한다.

int main()
{
    if ( nullptr ) { }
    
    bool b1 = nullptr; // error
    foo(nullptr);      // error
    nullptr << 10;     // error
    
    bool b2(nullptr);
    bool b3 = static_cast<bool>(nullptr);
}

 

※ 결론은 변환 연산자 사용을 자제하거나, 사용한다면 Explicit로 선언해주는것이 좋다.

'C++ > Basic' 카테고리의 다른 글

[C++] Operator Overloading IV  (2) 2024.08.26
[C++] Operator Overloading III  (0) 2024.08.25
[C++] Operator Overloading I  (0) 2024.08.23
[C++] Multiple Inheritance, Diamond Inheritance, Virtual Inheritance  (0) 2024.08.23
[C++] RTTI, Dynamic Cast  (0) 2024.08.22
Comments