C++을 사용하면서 항상 느끼는 것은 끝이 없다는 것이다. 처음에 C++을 어느정도 배운 후에는 배운 것에서 조금씩 덧붙여 가면 점점 쉬워 지겠지? 하는 생각이었다. 하지만 내 예상과 달리 C++은 사용할수록 많은 생각을 요구하고 반복적인 망각과 되새김 속에서 달팽이 처럼 힘겹게 기이가는 느낌이다.
그래도 힘 겨운 만큼 성취감도 크다. 물론 적성에 안맞는 사람들에게는 비추다. 차라리 파이썬과 같은 언어가 문법도 간단해서 일반적으로는 코드를 작성하기는 깔끔하고 훌륭하다.
오늘은 전위 증가, 후위 증가에 대해서 끄적여 보려하는데 사실 이 연산자들은 파이썬, 자바등 다른 언어들도 가지고 있다. 이해하기에도 어렵지 않고 그다지 쓸 내용은 없을 듯 보인다. 그럼에도 오늘 쓸만한 주제를 억지로 끄집어 내 보았다. 연산자 오버로딩과 관련한 내용인데 전위증가(사전증가), 후위증가(사후증가) 연산자 오버로딩의 선언이 왜 그렇게 선언되는지에 대한 내용이다. 이를 이해하기 위해서는 이 두 연산자가 어떻게 동작하는지 자세히 파악할 필요가 있다.
들어가기에 앞서 다음 코드를 테스트 해보자.
int i = 99;
i++ = 10000; // lvalue required as left operand of assignment
++i = 10000; // 문제 없음 ++i는 lvalue?
- 전위 증가연산자와 후위 증가 연산자의 메커니즘
전위 증가는 사전 증가라고도 하며 어떤 수식에 전위 증가 연산자가 포함되어 있다면 이 연산자는 해당 수식이 모두 계산된 뒤 동작한다.
쉬운 내용이므로 간단한 예만 들어보겠다.
...
int x = 2;
int result_A = 3*x++*2; //12
...
...
int x = 2;
int result_B = 3*++x*2; //18
....
두가지 예를 들었는데 두 연산의 차이점은 식에 포함된 x가 x++이냐 ++x냐의 차이다. 결과는 순서대로 12와 18이 나온다.
앞서 말했듯이 식이 계산되기 전에 1만큼 증가한 것이 ++x, 식의 계산이 끝난 후에 1만큼 증가되는 것이 x++이다. 그래서 결과 값도 다르게 나온다.
이 두 가지 경우를 언제 사용하냐에 대한 문제는 여기서는 논외로 하고 여기서는 단순히 문법적인 내용만 살펴보려고 한다.
컴파일러마다 이 두 연산이 동작하는 방법이 차이가 있을지 모르겠지만 이 두 연산자는 사실 모두 애초에 먼저 계산이 된다. 위 예에서 x와 y를 객체로 보면 이 두 연산의 결과가 반환될 때 어떻게 반환되는지를 살펴보면 좀 더 정확히 파악된다.
- 전위 증가 연산자는 lvalue를 반환
x의 값이 객체라고 가정하면 객체 내부에 2라는 값이 있을 것이다. 내부적으로 2의 값을 1만큼 증가시킨 후 참조형으로 반환한다. 즉 lvalue로 반환하는 것이다. 다시 말해 전위연산은 ++x가 포함된 수식이 계산되기 전에 연산되어지고 결과를 참조형으로 반환된다. 이 메커니즘은 어떤 문제점도 없이 1만큼 증가한 3을 반환하는 것이다. (정확히 말하면 3을 지닌 변수 또는 lvalue를 반환하는 것! C++표준은 전위 연산자가 lvalue로 반환하도록 하고 있다.) 처음 테스트 코드에서 ++i = 1000;이 문제없이 동작한 것도 이 때문이다.
- 후위 증가 연산자는 계산 전의 값을 반환
그렇다면 후위 증가의 경우는 어떠한가? x의 내부에 있는 2라는 값을 가진 변수가 있다면 이 변수의 값을 1 증가시키기 전에 사본를 만들어 놓는다. 그리고 원래 2를 가지고 있는 변수의 값을 1 증가 시킨 후 사본(2의 값을 가지고 있는 변수)을 반환한다.
따라서 후위 증가는 참조가 아닌 단순한 값을 반환한다.
그러므로 처음 테스트 코드에서 i++ = 1000;은 동작하지 않는다 에러 메세지를 보면 i++은 lvalue가 아닌 것을 확인할 수 있다.
이런 메커니즘은 전위, 후위 연산을 위한 것이고 그리 중요한 특징은 아닌 듯 보이지만 연산자 오버로딩을 할 때는 반환값을 제대로 정해줘야 하기 때문에 잘 알고 있어야 한다. 다시말해 전위 연산 오버로딩은 참조를 반환해야 하고 후위 연산자 오버로딩은 값을 반환하도록 오버로딩 해야한다.
다음 코드는 전위 후위 연산의 메커니즘을 코드로 나타낸 것이다.
#include <iostream>
using namespace std;
class Myfixtest {
private:
int i;
public:
Myfixtest(int i_):i(i_) {;}
Myfixtest operator+(const Myfixtest & other) {
Myfixtest temp(this->i + other.i);
return temp;
}
Myfixtest& operator++() {
this->i++;
return *this;
}
Myfixtest operator++(int) {
Myfixtest temp(*this);
this->i++;
return temp;
}
void print() {
std::cout << this->i << endl;
}
};
int main() {
Myfixtest A(3);
Myfixtest B(3);
Myfixtest result_A(A++);
Myfixtest result_B(++B);
result_A.print();
result_B.print();
A.print();
B.print();
}
코드를 실행해 보면
3
4
4
4
이런 결과가 나온다.
여기서 후위 연산자 오버로딩에 인수로 int를 넣어줬는데 이는 단순한 쓰레기 값이다. 형식적인 것으로 컴파일러에게 후위 연산이라는 것을 알리기 위한 것이다.
후위 연산이므로 A++ 또는 B++은 이 연산의 내용이 실행된다. 전위, 후위 연산은 단순하지만 메커니즘을 알아야 연산자 오버로딩의 형식이 왜 이런지 이해할 수 있을 것이다.
반응형