C++/STL

[C++ Container] vector push_back vs emplace_back

sseram 2023. 2. 22. 00:14
반응형

빠른 결론 : 엥간하면 push_back 쓰자

다만 내가 해당 구조에 대해서 제대로 알고 있고, emplace 가 더 효율적인 상황이라면 emplace_back이 더 효율적이니 사용을 고려해보자.

 

 

 

 

아래와 같은 class가 있다고 가정해 보자.

 

class myClass {
public:
	int mynum;
	myClass() {
		mynum = 0;
		cout << "default constructor" << endl;
	}

	myClass(int a) {
		mynum = a;
		cout << "constructor with " << a << endl;
	}

	myClass(const myClass& obj) {
		mynum = obj.mynum;
		cout << "copy constructor" << endl;
	}

	myClass& operator=(const myClass& obj) {
		this->mynum = obj.mynum;
		cout << "copy assign constructor" << endl;
		return *this;
	}

	myClass(myClass&& obj) noexcept {
		this->mynum = obj.mynum;
		cout << "move constructor" << endl;
	}

	myClass& operator=(myClass&& obj) noexcept {
		this->mynum = obj.mynum;
		cout << "move assign constructor" << endl;
		return *this;
	}

	~myClass() {
		cout << "destructor" << endl;
	}
};

 

 

 

무언가... 무언가를 하는 myClass 객체가 있다. 해당 객체는 본인이 어떻게 생성되었는지/ 파괴되는지를 유저에게 알려준다.

 

그리고 밑에 10개의 capacity를 가진, myClass를 담을 수 있는 Vector가 있다.

 

int main() {
	std::vector<myClass> myClassVec;
	myClassVec.reserve(10);		// capacity 설정.

	return 0;
}

 

 

 

해당 vector에 각각 다른 방법으로 myclass를 넣어서 결과를 확인해 보자.

 

 

1. 다른 곳에서 객체를 만들고, push back / emplace back으로 삽입.

 

int main() {
	cout << "==============PUSH BACK================" << endl;
	{
		std::vector<myClass> myClassVec;
		myClassVec.reserve(10);		// capacity 설정.
		myClass tmp(3);

		cout << "push back to myClassVec" << endl;

		myClassVec.push_back(tmp);
	}

	cout << "---------------------------------------" << endl;
	cout << endl << endl;
	cout << "==============EMPLACE BACK=============" << endl;
	{
		std::vector<myClass> myClassVec;
		myClassVec.reserve(10);		// capacity 설정.
		myClass tmp(3);

		cout << "emplace back to myClassVec" << endl;

		myClassVec.emplace_back(tmp); 
	}
	cout << "=======================================" << endl;
	return 0;
}

 

 

특이사항은 없다. tmp에서 만들어진 myClass 객체가 각각의 vector에 들어갈 때 본인을  copy하여 들어간다.

그 후 tmp가 파괴되고, myClassVec이 파괴되며 안에 들어가 있던 객체도 파괴된다.

 

즉. 3으로 만들어진 myClass 객체가 두 개 존재한다.

 

 

 

2. vector에 넣을 때 바로 객체를 만들기.

 

int main() {
	cout << "==============PUSH BACK================" << endl;
	{
		std::vector<myClass> myClassVec;
		myClassVec.reserve(10);		// capacity 설정.

		cout << "push back to myClassVec" << endl;

		myClassVec.push_back(myClass(4));

		cout << "push back end" << endl;
	}

	cout << "---------------------------------------" << endl;
	cout << endl << endl;
	cout << "==============EMPLACE BACK=============" << endl;
	{
		std::vector<myClass> myClassVec;
		myClassVec.reserve(10);		// capacity 설정.
		
		cout << "emplace back to myClassVec" << endl;

		myClassVec.emplace_back(myClass(4));

		cout << "emplace back end" << endl;
	}
	cout << "=======================================" << endl;
	return 0;
}

 

 

 

각 push back, emplace back 안에서 myClass라는 임시 객체가 만들어지고, 해당 객체가 myClassVec 안으로 이동한다.

임시 객체는 본인의 역할을 끝냈으니 사라지게 되고,  역시 중괄호 밖으로 나가면 vector가 사라지면서 다시 한 번 소멸자가 불리게 된다.

 

 

 

 

 

- 아니 그럼 emplace는 왜 있는거임?

-> 다음 경우를 보자.

 

 

 

3. myClass 생성에 필요한 인자를 전달하여 생성

int main() {
	cout << "==============PUSH BACK================" << endl;
	{
		std::vector<myClass> myClassVec;
		myClassVec.reserve(10);		// capacity 설정.

		cout << "push back to myClassVec" << endl;

		myClassVec.push_back(4);

		cout << "push back end" << endl;
	}

	cout << "---------------------------------------" << endl;
	cout << endl << endl;
	cout << "==============EMPLACE BACK=============" << endl;
	{
		std::vector<myClass> myClassVec;
		myClassVec.reserve(10);		// capacity 설정.
		
		cout << "emplace back to myClassVec" << endl;

		myClassVec.emplace_back(4);

		cout << "emplace back end" << endl;
	}
	cout << "=======================================" << endl;
	return 0;
}

 

 

 

뭔가 이상한 걸 넣는다.

 

분명히 myClassVec은 MyClass를 담는 객체인데 int를 넣고 있다. 가능한가?

-> 가능하다.

 

자세히 생각해보면...

 

	vector<string> strvec;
	strvec.push_back("someText");
	strvec.emplace_back("someText");

 

 

우리는 이러한  형태를 아무런 의심 없이 사용했다.

다만 자세히 생각해 보면, "someText" 는 string 형태가 아닌, const char* 형태라는 것을 알 수 있다.

ㄹㅇ 다름

 

그런데 자연스럽게 push_back 통해 vector에 넣고 있었다.

왜? 해당 인자로 string을 초기화 할 수 있기 때문에.

 

const char* 형태로 string을 초기화 할 수 있기 때문에, vector에서는 해당 객체의 초기화에 필요한 인자를 받아 내부에서 생성하여 vector에 추가해 준다.

 

 

-> const char* 형태는 string으로 암시적 변환이 가능하다.

const char* 형태를 가지고 string 형태로 형 변환을 한다. 형 변환을 하며 가지고 있는 재료로(여기선 const char*) 형 변환을 하려는 객체를(여기선 string) 만든다.

 

 

그러니, 위의 main 코드에서는 myClass 초기화에 필요한 '4' 라는 int형 인자를 받아 vector에서 직접 그 인자를 가지고 myClass를 생성하고, vector 뒤에 넣어준다.

 

-> 여기선 int를 가지고 myClass로 형변환을 한다. int를 재료로 myClass 객체를 만들 수 있으니 작성.

 

 

 

결과를 보자.

 

 

이번엔 좀 다르게 나왔다. push_back은 소멸자가 두 개이고 emplace_back은 소멸자가 한 개이다.

 

이유를 보면...

 

push_back :

1. 받은 초기화 인자를 가지고 임시 객체를 만든다. 

2. 해당 임시 객체를 vector 안에 이동시킨다.

3. 역할을 다 한 임시 객체를 delete한다.

 

이렇게, 기본 생성자 한 번. 이동 생성자 한 번. 소멸자 한 번 을 부르게 된다.

 

 

반면

 

emplace_back:

1. 받은 초기화 인자를 가지고, vector 내부에서 직접 객체를 만든다.

 

이렇게 기본 생성자 단 한 번만 사용하게 된다.

 

 

결국. 효율에 미친 사람들이라면 위의 방식대로 emplace_back을 사용하여 직접 인자를 넘겨주어, 생성자 두 번 부를 것을 한 번만에 해결할 수 있게 된다!

 

 

 

 

다만. 단점이 있다.

-

아래와 같은 상황.

myClass는 int 로만 초기화 할 수 있다. 이 때 저런 const char* 형태의 인자를 주면

 

push_back에서는 바로 불가능하다고 띄우지만

emplace_back 에서는 뭔 일인진 모르지만 아무런 불평 없이 받아들인다.

 

 

빌드 결과

물론 빌드하면 실패하고, 왜 실패했는지 알려주긴 한다.

지금이야 테스트 프로젝트이니 어서 잘못된 건지 금방 알 수 있게지만, 큰 프로젝트를 할 수록 어디에서 삑낫는지 찾기가 힘들어질 것이다.

 

 

다른 단점으로는.. push_back / emplace_back 하다가 exception이 뜰 때 문제가 되는데. exception이 떳을 때는 push_back이 더 안전하다고 하다.

이 부분은 공부를 좀 더 해봐야것다...

 

 

 

*잘못된 정보는 덧글로 알려주시면 감사하겠습니다.

반응형

'C++ > STL' 카테고리의 다른 글

[C++] std::async, std::thread (1)  (0) 2023.04.09
[C++17] std::filesystem::path  (0) 2023.04.04
[C++17 ] FOLD Expression  (0) 2023.03.29
[C++ String] raw String  (0) 2023.02.27
[C++ Container] map/unordered_map. at, []. element access 차이  (0) 2023.02.24