3 분 소요

! 틀린 내용이 있을 수 있습니다. 틀린 내용이 있다면 댓글 부탁드립니다.

모든 테스트는 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)  
  1. num의 메모리에 10을 대입한다.
  2. eax reg에 num의 값을 대입
  3. eax reg 값을 1 올려준다.
  4. 그리고 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)  
  1. num 메모리에 10 값을 대입한다.
  2. num값을 eax reg에 대입한다.
  3. eax 값을 rbp + 0x0D4에 저장한다.
  4. eax에 다시 num 값을 복사하고
  5. eax를 1 더해준다.
  6. num에 eax 값을 대입한다.
  7. 비교는 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
}

그렇다면 단순히 더하는 연산에서는 후위증감자와 전위증감자를 혼용해도 상관없을까? 차이가 없나?

결론을 말하자면 결국 후위증감자는 피연산자의 복사본을 넘기는 것이어서 차이가 있을 수 있다.
두 가지의 경우로 나눠서 생각해보자.

  1. primitive type - 큰 문제가 없을 가능성이 높다. 복사에 들어가는 비용이 매우 가볍다.
  2. 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 가 일어나게 된다.

후위증감자를 사용하게 되면 중간에 복사가 반드시 일어나게 되고, 이는 성능적으로 문제가 될 수 있다.

결론

  1. 전위증감자는 더해진 메모리가 반환된다.
  2. 후위증감자는 더해지기 전 값이 반환된다.
  3. class의 경우는 후위증감자를 사용하면 복사가 일어날 수 있으니 조심해야한다.
  4. stl iterator는 단순 포인터 연산만 하므로 어떤 것을 쓰든 성능적으로는 문제 없다.

일반적으로는 컴파일러가 전위연산자로 변경해주지만 습관적으로 후위증감자를 사용하지 않아도 되는 경우에는 전위증감자를 사용하는 것이 좋을 것 같다.

댓글남기기