일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- diamond inheritance
- c++ multi chatting room
- constructor
- conversion constructor
- c++ basic practice
- base from member
- std::cout
- this call
- std::ostream
- std::endl
- member function pointer
- virtual function table
- virtual destructor
- increment operator
- pointer to member data
- std::vector
- return by reference
- C++
- 더 지니어스 양면포커
- virtual inheritance
- operator overloading
- new&delete
- discord bot
- vector size
- delete function
- suffix return type
- placement new
- vector capacity
- virtual function
- dynamic_cast
- Today
- Total
I'm FanJae.
[C++] Operator Overloading II 본문
※ 본 포스트는 코드누리 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 |