push_back 함수와 emplace_back 함수의 차이를 알아보자.

 

대충은 알고 있었는데 자세히 알아보면서 기록하려고 한다. 

 

int 처럼 기본 자료형 말고 사용자가 만든 객체를 들고 있을때를 생각해보자.

 

A라는 클래스가 있다고 했을때

push_back(A(1,2));  --> ok
push_back(1,2); --> 생성자가 explicit 되어 있어 error!! explicit 이 없으면 가능

만약 생성자가 explicit 되지 않았다면 push_back(1,2)
는 A(1,2) 가 implicit 하게 호출되어 임시객체(Rvalue)를 만든다.

 

만약 A 클래스의 생성자가 explicit 으로 설정되어 있지 않다고 한다면,

 

push_back(1,2) 의 과정은 아래와 같다.

 

1. push_back을 통해 객체를 삽입하기 위해, 인자로 들어온 (1,2) 를 통해 A 의 임시(?) 객체(Rvalue)를 하나 만든다.

( 기본 생성자 )

 

2. 1번에서 만든 임시 객체를 이용해 복사 생성자(혹은 이동생성자가 정의 되어 있다면 이동생성자)를 통해 push_back() 함수 내에서 임시 객체를 만든다.

 

3. 함수 내에 만들어진 임시 객체를 vector 의 끝에 추가한다.

 

4. 함수를 빠져나온 후, push_back에 삽입하기 위해 만들었던 (1번) A의 임시 객체를 소멸시킨다.

 

 

 

Item 이라는 클래스를 통해 알아보자.

 

#include <iostream>
#include <vector>

using namespace std;

class Item {
public:

	Item(const int _n) : m_nx(_n) { cout << "일반 생성자 호출" << endl; }

	Item(const Item& rhs) : m_nx(rhs.m_nx) { cout << "복사 생성자 호출" << endl; }

	Item(const Item&& rhs) : m_nx(std::move(rhs.m_nx)) { cout << "이동 생성자 호출" << endl; }

	~Item() { cout << "소멸자 호출" << endl; }

private:
	int m_nx;
};

int main() {
	std::vector<Item> v;

	cout << "push_back 호출" << endl;
	v.push_back(Item(3));

	return 0;
}

좀 전과는 다르게 이동 생성자가(Move Constuructor) 가  정의되어 있기 때문에 복사생성자가 아닌 이동생성자(Move Constructor)가 호출된다.

순서를 알아보자면

1. Item(3) 을 통해 임시객체를 생성하고

2. push_back() 함수 안에서 복사 생성자나/ 이동 생성자를 통해 vector인 v 뒤에 들어갈 객체를 생성하고

3. 앞에 Item(3) 으로 생성한 객체를 없애준다.

4. 그 다음에 main 이 종료되면서 vector v가 stack 에서 없어지면서 자동으로 heap에 있는 벡터 데이터의 소멸자가 호출된다. 

 

 

 


 

주의 할 점이 있다.

아래와 같은 코드를 실행해보면 

 

 

 

오류가 뜨는데  이는 push_back() 함수 안에서 copy-constructor를 통해 pa를 copy 해 임시객체를 만드려고 해서 그렇다.

 

unique_ptr 은 명시적으로 delete를 통해 copy constructor를 못쓰게 해놨다.

왜냐면 복사생성자를 허락한다면 더 이상 unique 한 소유권을 갖는다는 말이 안맞기 때문이다.

 

대신에 move constructor는 허용하는데 마치 소유권을 넘기는것과 같다.

 

그렇기 때문에 std::move 를 통해 우측값 레퍼런스를 받는 버전이 오버로딩 될 수 있도록 해줘야 한다.

 

 


 

 

 

 

 

 

emplace_back은 c++11 에 도입된 함수로, 가변인자 템플릿을 사용하여 객체 생성에 필요한 인자만 받은 후 함수 내에서 객체를 생성해 삽입하는 방식이다.

 

emplace_back 함수는 전달된 인자를 완벽한 전달(perfect forwarding) 을 통해, 직접 template 클래스의 생성자에 전달 해서, vector 맨 뒤에 template 클래스 객체를 생성해버힌다.

 

 

emplace_back(1,2);  -> ok
emplace_back(A(1,2)); -> ok! 근데 이렇게 하면은 push_back(A(1,2)) 와 다를게 없다.

 

다시 말해, 임시 객체를 만들 필요가 없기 때문에, emplace_back 내부에서 삽입에 필요한 생성자 한번만 호출 된다. 

 

1. emplace_back 함수에 A 객체를 만들 수 있는 인자( 매개변수 )를 넘긴다.

 

2. emplace_back 함수 내부에서는 넘겨진 인자를 통해 직접 생성자를 호출하여 임시( 혹은 이름 없는?)객체를 만들어냄

즉, push_back 처럼 복사 생성자나 / 이동 생성자를 호출하지 않는다.

 

3. vector 에 객체 삽입

 

 

#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Item {
public:

	Item(const string _n) : m_sx(_n) { cout << "일반 생성자 호출" << endl; }

	Item(const Item& rhs) : m_sx(rhs.m_sx) { cout << "복사 생성자 호출" << endl; }

	Item(const Item&& rhs) : m_sx(std::move(rhs.m_sx)) { cout << "이동 생성자 호출" << endl; }

	~Item() { cout << "소멸자 호출" << endl; }

private:
	string m_sx;
};

int main() {
	std::vector<Item> v;

	cout << "emplace_back 호출" << endl;
	v.emplace_back(Item("dd"));

	return 0;
}

 

 

아래는 emplace_back 함수이다.

 cout << "emplace_back 호출" << endl;
 v.emplace_back(3);

 

예상 대로 emplace_back() 함수 안에서 이동 생성자이나 복사 생성자를 호출 하지 않는다.

 

 

그리고 이렇게 Item(3) 을 인자로 줘서 호출하게 되면 push_back() 함수와 똑같아진다.

cout << "emplace_back 호출" << endl;
v.emplace_back(Item(3));

 

 

push_back 이나 emplace_back을 한번 씩 더 해보자.

 

 

첫번째 호출 할 때는 예상했던 대로다.

 

그리고 한 번 더 호출 할 때, 중간에 복사생성자가 호출됐다. 

emplace_back 함수도 한 번 더 해보자.

 

복사생성자는 왜 생기는 걸까???

 

바로 밑에서 알아보자.

 

 

 


 

아래의 코드를 보자.

 

실행해 보면 아래와 같이 나온다.

 

여기 까지는 알던 내용이다.

( emplace 함수 내부에서 임시객체를 만들어서 vector에 삽입하니깐. )

 

 

여기서 추가적으로 

 

"nabi" 를 emplace_back 해주면은 결과는 아래와 같다. 

 

 

 

기존의 Cat 객체가 copy constructor 로 생성되는것보다 move constructor가 효율적이다.

 

그런데 왜 Cat 객체에 move constructor도 만들어 놨는데 move constructor가 실행되지 않는 것일까?

 

이유는 noexcept 를 붙혀주지 않아서 그렇다.

exception이 날 수도 있기 때문에 컴파일러가 안전하게 copy constructor를 불러준거다. 

 

noexcept 를 붙혀주면 아래와 같이 move constructor 가 불러지는것을 확인 할 수 있다.

# 22.8.25  - 일단 이렇게 기록해 놓는다, noexcept 에 대하여 조금 더 공부하고 추가로 기록하자.

 

 

 

그리고 참고로 (Rule of Three) copy 생성자copy 할당 연산자, 소멸자 를 아예 정의를 안해주면 Rule of five에 포함된 이동 생성자, 이동 할당 연산자 가 자동으로 암시적 생성되고, 위의 경우 이동 생성자가 호출된다.

* Rule of Three , Rule of Five  참고 : https://forward-movement.tistory.com/264

 

물론 가장 좋은 케이스는

미리 reserve 를 통해 공간을 확보하고 벡터에 원소가 추가 돼도 마이그레이션 없이 추가 되게끔 하는것이다. 

 


 

 

 

push_back으로 하여도 컴파일러 내부적으로 최적화 하기 때문에 emplace_back으로 하는 것과 별차이가 없을 수 있다.

고로 개인 프로젝트가 아니라면 호환성이 더 좋은 push_back 사용이 더 나을 수도 있다.

 

- push_back 함수로 할 수 있는 모든 것을 emplace_back으로 할 수 있다.

- push_back 함수보다 emplace_back 함수가 대체로 효율적이다.

 


 

 

C++17 부터는 reference 를 리턴 해주는 emplace_back() 함수가 하나 더 오버로딩 됐다.

 

그래서 아래와 같은 코드도 가능해졌다.

Cat& cat = cats.emplace_back("kitty",3); // reference를 리턴

 

하나 더 알아둬야 될 점은 emplace_back 함수는 가변인자 템플릿을 사용하는데, 정확히 Universal Refernce를 취하고 있다.

Cat nabi("nabi",4);
cats.emplace_back(nabi); // Lvalue
cats.emplace_back("baduk",5); // Rvalue

그렇기 때문에 emplace_back() 함수에서 

 

Lvalue 가 들어오면 Args& 로

Rvalue 가 들어오면 Arg 로

 

추론되기 때문에   결국  접힘 규칙? 으로 args 는 Arg& 혹은 Arg&&  타입이 된다. 

 

 

 

============================================================================================

 

#22.10.26 추가

 

아래가 너무 궁금한데 이유를 모르겠다...

 

struct A {

	int a = 10;
};

struct B :public A {
	int b = 15;
};


int main() {

	A* a = new A();
	
	B b;

	vector<A> list{};

	list.push_back(b);


	// 왜 위에거는 되고

	A* a = new A();

	B* b2 = new B();

	vector<A> list{};

	list.push_back(b2);

	//  아래거는 안되는거지??

 

 

 

 

 

 

 

 

 

 

 

 


아래의 블로그 들을 참고하였습니다. 

 

https://openmynotepad.tistory.com/category   <- C++ 마지막 페이지 에 있다.

 

'분류 전체보기' 카테고리의 글 목록

엄준식이 좋아요.

openmynotepad.tistory.com

 

 

https://shaeod.tistory.com/574

 

[C++ STL] std::vector - push_back

※ 요약 std::vector의 멤버 함수인 push_back에 대한 내용이다. 멤버 함수 push_back은 vector의 끝에 요소를 추가할때 사용하는 함수며, 이번 포스팅에서는 C++03과 C++11에서의 사용방법에 대해 간단히 알

shaeod.tistory.com

https://www.youtube.com/watch?v=GLt-D7w8hcg&list=PLDV-cCQnUlIZCF2lTD8GitKEHSqBbi69E&index=3 

 

+ Recent posts