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 함수를 통해 객체를 삽입하기 위해
1.v.push_back(Item(3)); 에서 Item(3) 을 통해 임시 객체를 하나 만들었음
2. 임시 객체를 이동생성자를 통해 push_back 함수 내부에서 임시 객체를 만들어냄
3. vector 에 객체 삽입
4. push_back에서 빠져나온 후 Item(3) 통해 만들어진 임시 객체 소멸
5. main 이 끝난 후 vector에 삽입된 객체 소멸
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은 매개 변수가 반복자 또는 호출후에, 유효하지 않은 객체를 참조하는 경우에 사용하는 것이 좋다.
int는 기본형이고, 표현하기위해 쓴것이지 실제로 동적 할당 변수를 가지고 있는 사용자 정의 타입 객체가 emplace_back을 사용할 시 정의되지 않는 동작이 발생했다.
std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // 일부 컴파일러에서는 잘못된 결과를 생성한다.
v.push_back(v[0]); // 임시객체로 생성하기 때문에 안전하다.
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개의 크기를 할당하므로 이런일이 발생한다. )
또한 축소 변환 등, 암시적인 변환에 대해 막기가 힘들다. 다음 예제를 보자.
#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을 사용하면, 삽입된 요소에 대한 참조를 반환한다.
'프로그래밍 언어 > C++' 카테고리의 다른 글
[C++] 매크로 개념과 주의사항 (0) | 2023.09.20 |
---|---|
[실4] 28279 - 덱2 (0) | 2023.09.19 |
[C] 배열과 포인터의 관계 (변수형 포인터, 상수형 포인터), 포인터로 배열 변경하기 (0) | 2023.09.17 |
[C] 포인터로 문자열 선언, 배열 문자열 선언과 차이. (문자열 내부 변경하기) (0) | 2023.09.17 |
[C] 문자열(string) 입출력 (puts, fputs, gets, fgets) 사용법 (0) | 2023.09.17 |