본문으로 바로가기

emplace_back 과 push_back 의 차이

category C++/Modern 2019. 1. 26. 03:22

item 타입의 생성자가 <string, int, int> 타입을 인자로 받는다면?

 

push_back 함수는 '객체' 를 집어 넣는 형식으로, 객체가 없이 삽입을 하려면 "임시객체 (rvalue) " 가 있어야 합니다.

또는 암시적 형변환이 가능하다면, 인자로도 삽입할 수 있습니다.

( 이는 인자를 통해 임시객체를 암시적으로 생성한 후 삽입합니다 )

vector<item> vt;
item a = {}; // 기본 생성자 호출

vt.push_back(item("abc", 1, 234)); 
vt.push_back(std::move(a)); 

vector<int> v;
v.push_back(1);
// 등등..

1. push_back을 통해 객체를 삽입하기 위해, item 임시 객체를 하나 만듭니다.

2. 임시 객체를 복사 생성자를 통해 push_back 함수 내에서 임시 객체를 만들어 냅니다.

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

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

 

emplace_back 함수는 C++11 에서 도입된 함수로서, 가변인자 템플릿을 사용하여 객체 생성에 필요한 인자만 받은 후

함수 내에서 객체를 생성해 삽입하는 방식입니다.

vector<item> vt

vt.emplace_back("abc",1,234);

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

 

 

#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));
 
 cout << "emplace_back 호출" << endl;
 v.emplace_back(3);

} 

push_back 함수 호출 결과

push_back 함수를 통해 객체를 삽입하기 위해 

1.v.push_back(Item(3)); 에서 Item(3) 을 통해 임시 객체를 하나 만들었음

2. 임시 객체를 이동생성자를 통해 push_back 함수 내부에서 임시 객체를 만들어냄

3. vector 에 객체 삽입

4. push_back에서 빠져나온 후 Item(3) 통해 만들어진 임시 객체 소멸

5. main 이 끝난 후 vector에 삽입된 객체 소멸

 

 

emplace_back 함수 호출 결과

emplace_back 함수를 통해 객체를 삽입하기 위해

1. v.emplace_back(3); 통해 emplace_back 함수에 Item 객체를 만들 수 있는 인자 ( 매개변수 ) 를 넘겼음.

2. emplace_back 내부에서 임시객체를 만들어냄

3. vector 에 객체 삽입

4. main 이 끝난 후 vector에 삽입된 객체 소멸

 

보통의 결과에선 emplace_back이 push_back보다 빠르다고 할 수 있다.

물론 컴파일러마다 최적화를 해 push_back도 emplace_back과 비슷한 성능 혹은 더 빠른 성능을 낼 수도 있다고 하지만

대부분의 경우에는 emplace_back이 빠르다고 한다. 

그럼 push_back을 사용하는 이유는 무엇인가?

 

이 두 함수의 실제 차이점은 emplace_back 이 모든 유형의 생성자를 호출한다는 것이다.

대신 push_back은 암시적인 생성자만 호출한다.

만약 T에서 U로 암시적으로 변환이 가능하다면, U는 손실없이 T의 모든 정보를 유지한다.

 

std::vector<std::unique_ptr<T>> v;
T a;
v.push_back(std::addressof(a)); // a는 포인터 형식이 아니기 때문에 삽입할 수 없음!!
v.emplace_back(std::addressof(a)); // ok, 컴파일할때는 에러가 발생하지 않음.

// (addressof 함수는 T형의 객체가 & 연산자를 오버로딩 하더라도, 그 객체의 실제 주소를 가져옵니다.)

std::unique_ptr<T>에는 T*의 명시적 생성자가 있습니다. (T*을 오버로딩했기 때문에)

emplace_back 함수는 명시적 생성자를 호출할 수 있기 때문에,

소유하지 않은 포인터를 전달하면 정상적으로 컴파일 됩니다.

 

하지만 v가 범위를 벗어날 때 소멸자는 포인터를 delete 하려고 할 것 이고, 이 포인터는 스택 객체이므로

( new 로 할당되지 않았으니 delete 할 수 없음 )

 

삭제 도중 정의되지 않는 동작이 발생합니다.

 

==> push_back은 T*를 넘기는데 vector<unique_ptr<T>> 와 호환이 되지 않는다.

하지만 emplace_back은 가변인자 템플릿이므로 모든 유형의 생성자를 호출하며 그 중 

unique_ptr<T*>의 명시적 생성자가 있기 때문에 오류가 난다고 하는 것.

 

push_back은 매개 변수가 반복자 또는 호출후에, 유효하지 않은 객체를 참조하는 경우에 사용하는 것이 좋습니다.

 

std::vector<int> v;

v.emplace_back(123);
v.emplace_back(v[0]); // 일부 컴파일러에서는 잘못된 결과를 생성합니다.
std::vector<int> v;

v.emplace_back(123); 
v.push_back(v[0]); // 임시객체로 생성하기 때문에 안전합니다.

int는 기본형이고, 표현하기위해 쓴것이지 실제로 동적 할당 변수를 가지고 있는 사용자 정의 타입 객체가

emplace_back을 사용할 시 정의되지 않는 동작이 발생했습니다.

 


흥미로운 글을 발견했습니다. 링크

즉, emplace_back과 push_back은 '더 효율적인' 같은 수식어는 붙을 수 없다. 입니다.

 

#include<vector>
#include<iostream>

int main(){
  // Basic example
  std::vector<int> foo;
  foo.push_back(10);
  foo.emplace_back(20);

  // More tricky example
  std::vector<std::vector<int>> foo_bar;
  //foo_bar.push_back(10); // Throws error!!!!
  foo_bar.emplace_back(20); // Compiles with no issue
  std::cout << "foo_bar size: " << foo_bar.size() << "\n";
  std::cout << "foo_bar[0] size: " << foo_bar[0].size() << "\n";
  return 0;
}

이 코드를 보면, 'std::vector<std::vector<int>> foo_bar' 로 이중 벡터가 선언되어 있습니다.

push_back을 통하여 데이터를 넣으려면 

vector<int> v{1, 2};
foo_bar.push_back(std::move(v));

이런 형식으로 데이터를 넣어야 하는데, 

foo_bar.push_back(10); // Throws error!!!!

다음과 같이 넣어버리면, 오류가 발생합니다.

하지만 emplace_back에서는 오류가 발생하지 않습니다.

  foo_bar.emplace_back(20); // Compiles with no issue

이는 foo_bar 안의 vector[0] 에게 20개의 새로운 요소를 생성하라는 것과 동일하기 때문에 이 일이 발생합니다.

( 즉, foo_bar안에서 20이라는 파라미터가 들어가게되면, 내부에서는 vector의 마지막 다음 요소에 20이란 값을 가지고 새로운 객체를 생성한다. 하지만 vector는 20이란 숫자가 들어가면, 20개의 크기를 할당하므로 이런일이 발생한다. )

 

emplace_back 내부의 생성

또한 축소 변환 등, 암시적인 변환에 대해 막기가 힘듭니다. 다음 예제를 보자.

#include <vector>
class A {
 public:
  explicit A(int /*unused*/) {}
};
int main() {
  double foo = 4.5;
  std::vector<A> a_vec{};
  a_vec.emplace_back(foo); // No warning with Wconversion
  //A a(foo); // Gives compiler warning with Wconversion as expected
}

컴파일 타임에서 에러를 잡아내지 못하기 때문에, 런타임에서 의도하지 않은 행동이 발생하게 된다.

 

위의 링크에서는 '다중 파라미터'를 이용하여 생성자를 호출할 때 사용하라고 적혀있다.

class Image {
  Image(size_t w, size_t h);
};

std::vector<Image> images;
images.emplace_back(2000, 1000);

image 같은 데이터를 push_back을 이용하여 추가하려면 새로운 객체를 생성하고 push_back을 호출하여야 하는데,

생성자만을 이용하여 호출하면 '이동'을 하지 않으므로 조금 더 가볍게 사용할 수 있습니다.

즉 -> '이동' 작업이 비쌀 때만 사용하여야 합니다.

 

또한 C++17에서부터는 emplace_back을 사용하면, 삽입된 요소에 대한 참조를 반환합니다. 이를 사용할때도 사용합니다.

 

참조 링크 : 

https://shaeod.tistory.com/630 

https://code.i-harness.com/ko-kr/q/a62d9d 

https://gumeo.github.io/post/emplace-back/

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

C++11) std::forward (perfect forwarding)  (4) 2019.02.02
c++11) std::move (move semantics)  (2) 2019.02.01
C++11) auto 키워드  (0) 2019.01.30
C++11) 범위 기반 for문 (loop)  (4) 2019.01.27
C++11) 초기화 리스트(initialize_list)  (2) 2019.01.27