[C++] 전위/후위증감자의 이해
! 틀린 내용이 있을 수 있습니다. 틀린 내용이 있다면 댓글 부탁드립니다.
모든 테스트는 visual studio 22 (v143) 으로 진행했습니다.
정리
전/후위 증감자란
expression 앞 뒤에 ++, – 를 붙여서 값을 1씩 늘리거나 감소하는 연산자이다.
++unary-expression
--unary-expression
unary-expression++
unary-expression--
차이점
둘 다 피연산자의 값이 늘어나거나 감소하는 것은 동일하지만 결과 타입에서 차이가 난다.
전위 증감자는 lvalue, 후위 증감자는 rvalue가 결과값으로 반환된다.
어셈블리 확인
그렇다면 실제로 어떻게 작동하는지 체크해보자.
visual studio 2022 에서 실행한 코드이다.
전위증감자
int main()
{
int num = 10;
if(++num == 11)
{
cout << num << '\n';
}
return 0;
}
어셈블리 코드
int num = 10;
00007FF64ACD245B mov dword ptr [num],0Ah
if (++num == 11) {
00007FF64ACD2462 mov eax,dword ptr [num]
00007FF64ACD2465 inc eax
00007FF64ACD2467 mov dword ptr [num],eax
00007FF64ACD246A cmp dword ptr [num],0Bh
00007FF64ACD246E jne main+4Ah (07FF64ACD248Ah)
- num의 메모리에 10을 대입한다.
- eax reg에 num의 값을 대입
- eax reg 값을 1 올려준다.
- 그리고 11과 num의 값을 비교해서 if 비교
비교하는 주체가 eax가 아닌 num이다.
후위 증감자
int main()
{
int num = 10;
if(num++ == 11)
{
cout << num << '\n';
}
return 0;
}
어셈블리로 바꾸면
int main() {
00007FF7E29D2440 push rbp
00007FF7E29D2442 push rdi
00007FF7E29D2443 sub rsp,108h
00007FF7E29D244A lea rbp,[rsp+20h]
00007FF7E29D244F lea rcx,[__6E4D752D_main@cpp (07FF7E29E306Ah)]
00007FF7E29D2456 call __CheckForDebuggerJustMyCode (07FF7E29D13DEh)
int num = 10;
00007FF7E29D245B mov dword ptr [num],0Ah
if (num++ == 11) {
00007FF7E29D2462 mov eax,dword ptr [num]
00007FF7E29D2465 mov dword ptr [rbp+0D4h],eax
00007FF7E29D246B mov eax,dword ptr [num]
00007FF7E29D246E inc eax
00007FF7E29D2470 mov dword ptr [num],eax
00007FF7E29D2473 cmp dword ptr [rbp+0D4h],0Bh
00007FF7E29D247A jne main+48h (07FF7E29D2488h)
00007FF7E29D247C mov dword ptr [rbp+0D8h],1
00007FF7E29D2486 jmp main+52h (07FF7E29D2492h)
00007FF7E29D2488 mov dword ptr [rbp+0D8h],0
00007FF7E29D2492 cmp dword ptr [rbp+0D8h],0
00007FF7E29D2499 je main+75h (07FF7E29D24B5h)
- num 메모리에 10 값을 대입한다.
- num값을 eax reg에 대입한다.
- eax 값을 rbp + 0x0D4에 저장한다.
- eax에 다시 num 값을 복사하고
- eax를 1 더해준다.
- num에 eax 값을 대입한다.
- 비교는 rdp + 0D8h 와 비교한다.
비교하는 주체가 더하기 전 num 의 값이다.
결과
위의 결과에서 알 수 있듯이 전위증감자는 더한 후 그 변수가 반환되고,
후위증감자는 더하기 전의 값이 반환된다.
즉 전위증감자는 lvalue가 반환되고, 후위증감자는 rvalue가 반환되는 것을 뜻한다.
이 결과는 아래의 컴파일 에러가 왜 생기는가에 대해서 알 수 있게해준다.
int num = 10;
++num = 12; // ok
num++ = 12; // error. 식이 수정할 수 있는 lvalue여야 합니다.
추가 고민
무엇을 써야할까?
코드를 보면 이와 같이 ++i 와 i++ 를 혼용해서 사용하는 경우가 많다.
int size = 10;
for(int i = 0; i < size; ++i) {
for(int j = 0; j < size; j++) {
// do somthing
}
}
std::vector<int> v = {1, 2, 3, 4};
for(auto it = v.begin(); it < v.end(); it++) {
// do something
}
그렇다면 단순히 더하는 연산에서는 후위증감자와 전위증감자를 혼용해도 상관없을까? 차이가 없나?
결론을 말하자면 결국 후위증감자는 피연산자의 복사본을 넘기는 것이어서 차이가 있을 수 있다.
두 가지의 경우로 나눠서 생각해보자.
- primitive type - 큰 문제가 없을 가능성이 높다. 복사에 들어가는 비용이 매우 가볍다.
- class - 문제는 이 경우에 대해서 생길 수 있다.
class 후위증감자
위에서 사용한 vector iterator 를 예시로 확인해보면
// visual studio 22
_CONSTEXPR20 _Vector_const_iterator operator++(int) noexcept {
_Vector_const_iterator _Tmp = *this;
++*this;
return _Tmp;
}
이렇게 temp에 자신을 복사하는 과정이 들어가 있다. (stl iterator는 primitive 수준으로 작동하므로 문제가 되지 않는다. 로직 이해를 위한 코드이다.)
예를들어서
class Foo {
public:
Foo()
:
data(new char[10000]),
size(0)
{
data[size] = 0;
}
Foo(const Foo& o)
:
data(new char[10000]),
size(o.size)
{
memcpy(data, o.data, size);
data[size] = 0;
}
~Foo() {
delete[] data;
}
Foo& operator=(const Foo& o) {
this->size = o.size;
memcpy(this->data, o.data, size);
this->data[size] = 0;
}
Foo operator++() {
this->data[size++] = '1';
this->data[size] = 0;
return *this;
}
Foo operator++(int) {
Foo tmp = *this;
this->data[size++] = '1';
this->data[size] = 0;
return tmp;
}
private:
char* data;
int size = 0;
};
int main() {
Foo f;
f++;
return 0;
}
내부에 string을 가지고 있고, 증감연산자로 뒤에 1을 붙이는 class가 있다고 했을 때 실행 순서를 보면
f++
가 실행되면 Foo tmp = *this;
가 실행되고 operator=
가 실행되면서 결국 memcpy
가 일어나게 된다.
후위증감자를 사용하게 되면 중간에 복사가 반드시 일어나게 되고, 이는 성능적으로 문제가 될 수 있다.
결론
- 전위증감자는 더해진 메모리가 반환된다.
- 후위증감자는 더해지기 전 값이 반환된다.
- class의 경우는 후위증감자를 사용하면 복사가 일어날 수 있으니 조심해야한다.
- stl iterator는 단순 포인터 연산만 하므로 어떤 것을 쓰든 성능적으로는 문제 없다.
일반적으로는 컴파일러가 전위연산자로 변경해주지만 습관적으로 후위증감자를 사용하지 않아도 되는 경우에는 전위증감자를 사용하는 것이 좋을 것 같다.
댓글남기기