강의/computer science

[computer science] c++의 Copy/Move semantics, Static

하기싫지만어떡해해야지 2024. 10. 15. 15:56

이 게시글은

서울대학교 데이터사이언스대학원

조요한 교수님의

데이터사이언스 응용을 위한 컴퓨팅 강의를

학습을 위해 재구성하였습니다.


파이썬으로 프로그래밍을 할 때

아직 프로그래밍을 잘 모르던 시절

 

int_list = [1, 2, 3]

new_int_list = int_list

new_int_list.append(4)

 

위와같이 프로그래밍을 해줬는데

int_list에도 4가 append되어있어

당황했던 기억이 있을 것이다

 

그래서 막 구글링을 하다보면

new_int_list = copy.deepCopy(int_list)

와 같이 해야하는데

그 이유는 deep copy를 해야

int_list의 요소들만 담은

완전히 새로운 new_int_list가

생성된다는걸 들어본적이 있을 것이다

 

프로그래밍을 공부하다보면

call by value

call by reference

개념에 대해 종종 듣게되고

copy의 개념에 대해서 듣게되는데

오늘은 이와 관련된 내용을

정리해보려고한다

 

 

Copy Semantics

 

위 코드를 실행하면 어떤 결과가 나올까?

교수님이 학생들께 질문하셔서

이런저런 답변이 나왔지만 결과는

 

런타임 에러가 뜬다고한다;

 

나도 사실 에러가 뜰거라고는

생각도 못해서..

왜 런타임 에러가 뜨는지 알아보자

 

 

차례차례 한 번 살펴보자

vec2를 만들면서 vec1의 내용을

copy해서 초기화한다

 

이럼 vec2가 copy하는 것은

vec1의 메모리 주소를 복사해서

가져오는 것이다

 

한마디로 vec2에 addElement를 해주면

복사된 vec1의 메모리 주소에 접근해서

addElement를 한다는 뜻

 

위의 파이썬에서 아예 새로운 list를

생성하려면 deepCopy를 해줘야하는 이유도

이와 유사하다

 

아무튼 그렇게 vec2에 vec1의

메모리 주소를 복사한 다음

vec1.addElement(3);을 해준다

 

그럼 ppt에서 오른쪽을 보면

기존의 vec1의 size는 2, capacity도 2였다

그런데 요소를 한개 추가했으니

capacity가 증가해야한다

그럼 내부에서 capacity를 증가시켜서

새로운 메모리 주소에

vec1을 할당한다

 

그럼 vec1은 새로운 메모리 공간에

새로 할당되어

{1, 2, 3}과 같은 요소를

갖게 되는 것이다

 

밑에 있는

vec2.addElement(4)도 마찬가지

capacity를 증가시킨

새로운 메모리 주소에

{1, 2, 4}처럼 할당된다

 

이렇게 vec1과 vec2에

각각 addElement를 해주면 결국

 

같은 메모리 주소를 가리키고있던

vec1과 vec2는

각각 다른 메모리 주소로 할당되게되고

 

main함수가 종료되면서

컴파일러가 vec1과 vec2가

차지한 공간을 free시키게 되는데

이때 기존에 처음에 있던

{1, 2}가 존재했던 공간을

vec1을 free할 때도

vec2를 free할 때도

두번씩 free해주면서

runtime error가 발생하게된다

(사실 이렇게 들은 것 같은데

자세하게 기억이 안난다..

혹시 정확하게 왜 이게 runtime error가

뜨는지 아시는 분이있다면 댓글을..)

 

따라서 어떤 변수를 복사해서

완전히 새로운 변수로 사용하고싶다면

deep copy가 필요하다

 

 

 

object를 초기화하거나

update 할 때

copy semantics가 발동한다

 

새로 할당할 때 vec으로 초기화해주거나

vec1 = vec2 이렇게 해주거나

함수의 이자를 넘겨줄 때 copy가 발생한다

 

 

Copy Constructor

 

copy constructor는

특별한 constructor이다

 

언제 호출되냐하면

class의 object를 새로 만드는데

기존에 존재하는 object로 초기화를 할 때

copy constructor가 호출이 된다

 

ppt 오른쪽의 첫 번째

코드 케이스를 보면

vec을 먼저 생성한다음

copiedVec을 초기화하면서 vec을

할당해준다

 

그다음 두 번째 케이스 같은 경우는

함수의 인자로 myVec을 넣어주는데

이때도 copy constructor가 호출된다

 

 

위는 copy constructor를

정의한 코드이다

 

input으로는 복사할 src의

reference를 받아온다

 

copy constructor 안에서

array에 capacity만큼 dynamic하게

공간을 할당해준다 

 

단순히 메모리 주소를 복사하는게 아니고

src에 있는 element들을

하나씩 넣어준다

 

deepcopy 복사를 하는 것이다

 

반드시 이렇게 구현하는게 정답은 아니지만

deepcopy를 할 수 있는

copy constructor 정의라고 생각하면된다

 

보통 copy constructor에서는

return값이 없다

 

 

Copy Assignment Operator

 

이미 initialize가 끝난

object를 다른 object로 

할당할 때는

copy constructor가 아닌

copy assignment operator가 호출된다

 

 

operator=를 정의해주고

반환값은 SimpleVector의 reference값

input으로는 SimpleVector인 src를 넣어준다

 

그런다음 기존에 있는 array는

어차피 안쓸 것이기 때문에

free를 시켜준다

 

src의 capacity만큼

array를 동적으로 할당해주고

src의 element를 array에 넣어준다

 

그런 다음 자기 자신을 반환한다

 

 

Move Semantics

 

Move Semantics에 대해서 소개해보려한다

앞에서는 copy semantic에 대해 정리했는데

말그대로 deep copy를 하는 과정이다

 

하지만 당연히

deep copy를 하게되면

기존에 있던 요소를 다시 새로 할당하므로

공간도 많이 사용되고

연산량도 많아진다

 

이런 deepcopy는 굉장히 비싼 작업이므로

최대한 피하자는 의도에서 나온게

move semantic이다

 

만약 input으로 받아왔거나

초기화시켜줄 src가

temporary하게만 필요해서

잠깐 쓰고 나중에 필요가 없어질거라면

copy를 하지말고 그냥 transfer를 시켜주는 것이다

 

위 ppt의 첫 번째 코드 예시를 보면

existingVec을 SimpleVector<int>{3, 4}로

다시 할당하고 있는데

이 할당이 끝나면 SimpleVector<int>{3, 4}는

더이상 쓸모가 없게된다

 

또 두 번째 예시에서

SimpleVector를 새롭게 생성하는

createVec()이라는 함수에서

 vec이라는 SimpleVector를 정의한다음

반환값으로 내어주고있다

 

이렇게되면 createVec이라는 함수 내부에서

변수가 생성된 뒤

vec이라는 변수는 createVec이 끝나면

더이상 필요가 없게된다

 

이러할 때

copy가 아닌 transfer를

해주자는 것이다

 

 

기본적으로 constructor는

무언가를 초기화할 때 발동한다

 

move constructor은

위의 ppt에서

4가지 경우에 발동한다

 

마지막 같은 경우는

vec1이 원래는 temporary하게

사용되는 변수가 아니지만

사용자가 move(vec1)과 같이

vec1은 temporary하게

사용해도 된다고 표현해주면

이러한 경우에도

vec2를 초기화 할 때

move constructor가 호출된다

 

결론적으로

move constructor를 사용하는 이유는

어떤 object를 다른 object의 상태로

초기화를 시켜주려고 하는데

메모리나 리소스를 효율적으로 해주고싶기 때문인 것이다

 

 

 

move constructor의 구현부이다

여기서는 인자를 어떻게 정의했는지가 중요한데

인자로 SimpleVector 뒤에 &&를 붙여주는 이유는

move constructor라는 것을

알려주기 위함이다

 

뒤의 noexcept는

큰 의미는 없지만

compiler에게 이 메소드를 호출할 때는

exception을 던지지않겠다고 알려주는 부분이다

 

constructor 구현부를 보면

array, size, capacity를

src의 것으로 초기화해준다

 

여기서 array(src.array)로 해주는데

이는 아까 요소를 한개한개 새로 넣어주었던

deepcopy와 다르다

이는 그냥 src의 메모리 주소만 넘겨주는 것이다

 

그런 다음 src는 이제

사용되지않는다고 가정한 뒤

array는 nullptr로

size와 capacity는 0으로 변경해준다

 

src를 move 한 이후에

src의 상태는 여전히 valid해야한다

정확히 src가 어떤 상태여야하는지는

unspecified지만

어쨌든 valid한 상태여야하기때문에

위와같이 src의 요소들을

정의해주는 것이다

 

 

 

assignment operator는

초기화가 아닌 update를 할 때 호출된다

 

constructor -> initialize

assignment operator -> update

로 기억하면 좋을 것 같다

 

어떤 object를 temporary한 어떤 상태로

update시킬 때 호출된다

 

ppt에서 위의 3가지 상황에

move assignment operator가 호출된다

 

 

 

move assignment operator의 구현부이다

move의 구현(?)이므로

역시나 Input값에는 &&를 붙여주고

noexcept도 붙여준다

 

맨 처음에 this != &src를 하는 이유는

src가 지금 자기자신의 object와

다를 때만 move를 수행하기 위해서이다

 

따라서 어차피 갖고있던 array는

안쓸 것이므로 delete를 해주고

 

다시 새로 array에

src.array를 넣어주고

size와 capacity도 넣어준다

 

그런 다음 src의 요소들은

nullptr, 0, 0을 다시 넣어줘서

valid하지만 unspecified하게 해준다

 

 

 

Copy Semantics와

Move Semantics를

비교구분한 table이다

 

constructor -> 초기화할 때

assignment operator -> update할 때

 

copy -> input으로 받아오는게 non-temporary

move -> input으로 받아오는게 temporary

 

이렇게 구분해서 기억하면 좋을 것 같다

 

 

 

어떤 class가 resource를 관리할 때

5개의 special한 member들은

구현을 해줘야한다는 법칙이있다

 

그 구현을 해줘야하는 것이

Destructor

Copy Constructor

Copy Assignment Operator

Move Constructor

Move Assignment Operator

 

5가지이다

 

이걸

The Rule of Five

라고 부른다

 

 

 

return value optimization(RVO) 대해

간단하게 알아보자

 

compiler는 최대한 리소스를

최적화하려고한다

 

그런 compiler의 최적화를 위해

적용되는 법칙 중 하나가

return value optimization이다

 

저 ppt 위의 코드를 자세히보면

createVector()라는 함수ㅇ내에서

vec{1, 2}를 생성해주고

main함수에서

vec에 createVector를 할당해준다

 

우리가 이전에 배운 내용을

바탕으로 한다면

createVector안에 있는 vec과

main함수에 있는 vec은

메모리 주솟값이 달라야한다

 

왜냐하면 compiler가

stack에 적재하는 방식을 보면

createVec을 위한 공간이 따로있고

그 안에 createVec 내부에 있는

vec 변수의 메모리 주솟값이

존재하기 때문이다

 

따라서 main함수를 실행할 때

생성되는 vec과는

완전히 별개의 주소에

생성될텐데

지금 저 위의 출력결과를 보면

createVector에 있는 vec과

main함수에 있는 vec의

주솟값이 동일한 것을 확인할 수 있다

 

왜일까?

 

이런걸 바로

Return Value Optimization

이라고 한다고 한다

 

컴파일러가 어차피 createVector에서

만든 vec을 main에서 쓸 것이란걸

알기 때문에 vec이라는 변수를 생성할 때

main 함수의 위치에서 생성하는 것이다

 

불필요한 copy나 move를

없애기 위해 최적화를 하는 것이다

 

 

 

Static Members

 

마지막으로

Static 변수에 대해 알아보자

 

 

 

vec1, vec2, vec3 안에 있는

요소들 중에서

1, 2, 3, 4, 5, 6의 개수를

각각 세보고싶다

 

이럴때 저 elementCount라는 값은

cclass 전체에서 접근할 수 있도록해야

개수를 더하고 빼주기가 쉬울 것이다

 

이럴 때 사용할 수 있는게

바로 static member이다

 

static member는 결국

class에 속해있는 member들인데

모든 Object들이 다

접근할 수 있는 변수이다

생성자 없이도 접근할 수 있다

 

따라서 보통 이런 static 변수는

모두가 공유하는 데이터인

shared data

class-wide information

utility function

에 자주 사용한다

 

 

c++에서 static은 다음과 같이

선언한다

 

앞에 static을 붙여준 뒤

변수를 선언해준다

 

그럼 elementCount는

딱 1개만 생성이되고

이후 모든 object들이

elementCount를 가져다 쓸 수 있다

 

하지만 초기화는

class 바깥에서

해줘야한다고 한다

 

public, private으로

accessibility도

정의가 가능하다고한다

 

 

사용 예시를 한 번 보자

 

SimpleVector class안에

printElementCount라는

static함수를 한 개 선언하고

그 밖에서 함수를 구현해줬다

 

그런 다음 main함수에서

SimpleVector<int>::printElementCount()와 같이

따로 생성자를 생성해주지않고

바로 접근하고있다