-
생각보다 자주 보이는 생성자개발/C·C++ 2021. 8. 4. 22:46
임시 객체는 시스템에 부담을 주는 존재입니다. C++에서 이동 의미론이나 rvalue reference 같은 개념을 도입한 이유이기도 할 겁니다. copy elison(복사 생략)처럼 임시 객체를 이용해서 초기화가 이루어지는 경우, 직접 초기화하는 것처럼 컴파일러가 최적화를 해주는 것도 같은 맥락일 것입니다. copy elison을 이용할 수 있는 환경이 아니라면 이동 의미론을 활용하는 편이 좋다고 생각합니다. 간단한 예제 몇 가지를 들어보겠습니다. 다음과 Person 클래스가 있습니다.
class Person { public: Person(int data) :_data(data) { cout << "Person(int data)->" << _data << endl; } Person(const Person& rhs) :_data(rhs._data) { cout << "Person(const Person& rhs)->" << _data << endl; } Person(Person&& rhs) noexcept :_data(rhs._data) { cout << "Person(Person&& rhs)->" << _data << endl; } friend std::ostream& operator<<(std::ostream& os, Person& person) { return cout << person._data; } private: int _data; };
ㅇ
Person 클래스의 객체를 map에 넣어보겠습니다.
int main() { std::map<int, Person> mp; mp.insert({ 1, Person(1) }); mp.insert({ 2, Person(2) }); mp.insert({ 3, Person(3) }); }
insert()가 한 번 호출될 때마다 이동 생성자가 두 번씩 호출되고 있습니다. insert에서 pair 객체를 초기화할 때 한 번 트리 노드를 초기화할 때 한 번(추정)입니다. 이동 생성자가 없으면 복사 생성자가 호출됩니다. 복사 생성자에서 힙 할당 등의 리소스 소모가 큰 루틴이 들어가 있다면, 성능 저하가 있을 수 있습니다.
foreach를 이용해서 mp의 원소에 접근해보겠습니다. 임시변수는 값 복사를 막기 위해 lvalue reference를 사용하고 실수로 데이터를 수정하는 것을 막기 위해 const를 붙였습니다.
int main() { std::map<int, Person> mp; mp.insert({ 1, Person(1) }); mp.insert({ 2, Person(2) }); mp.insert({ 3, Person(3) }); cout << endl; for (const std::pair<int, Person>& person : mp) { // 생략 } }
복사 생성자가 호출되어버렸습니다. 뭐가 문제였을까요? 코드에서 const를 떼어보면 알 수 있습니다. mp 원소들에 접근할 수 있는 정확한 타입은 std::pair<const int, Person>입니다. 타입이 다르기 때문에 복사가 일어났던 것입니다. 원소에 const를 붙이면 객체 자체가 상수화되므로, 허용해주만 정확한 타입이 아니기에 값 복사를 통해 임시 객체가 초기화됐습니다. 키 타입인 int에 const를 붙여야 정확한 타입이 완성됩니다. auto 키워드를 사용하면 가독성이 올라가면서 실수를 원천 차단 할 수 있습니다.
마지막으로 벡터 예제를 하나 보겠습니다.
int main() { std::vector<Person> vp; vp.reserve(4); vp.emplace_back(Person(1)); vp.emplace_back(Person(2)); vp.emplace_back(Person(3)); }
벡터 객체를 만들고 4 크기의 메모리 공간을 확보했습니다. reserve()는 메모리 공간만 확보해놓는 데 반해, 객체를 만들 때 갯수를 지정해주거나 resize()를 호출하면 메모리를 확보하고 객체까지 만들어버립니다. 다음 그림처럼 말이죠. 때문에 디폴트 생성자가 없으면 오류가 발생합니다.
코드를 다음과 같이 수정하면 어떤 일이 일어날까요.
int main() { std::vector<Person> vp; vp.reserve(4); vp.emplace_back(Person(1)); vp.emplace_back(Person(2)); vp.emplace_back(Person(3)); cout << endl; vp.reserve(5); }
메모리를 새로 확보하면서 기존 데이터이 이동된 모습을 확인할 수 있습니다. 이 때 이동 생성자에 noexcept가 없으면 복사가 일어납니다. 이유는 라이브러리를 살펴보면 알 수 있습니다. reserve() 내부적으로 _Umove_if_noexcept()를 사용해 noexcept가 선언되어 있으면 이동 함수를, noexcept가 없으면 복사 함수를 호출합니다.
'개발 > C·C++' 카테고리의 다른 글
[Rvalue reference] Forcing Move Semantics(4) (0) 2021.08.26 rvalue reference is a reference (0) 2021.08.04 [알고리즘] Quick sort (0) 2021.08.03 [자료구조] doubly linked list (0) 2021.08.02 [overloading] operator << (0) 2021.07.28