이 게시글은
서울대학교 데이터사이언스대학원
조요한 교수님의
데이터사이언스 응용을 위한 컴퓨팅 강의를
학습을 위해 재구성하였습니다.
파이썬으로 프로그래밍을 할 때
아직 프로그래밍을 잘 모르던 시절
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()와 같이
따로 생성자를 생성해주지않고
바로 접근하고있다