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++ 마지막 페이지 에 있다.
https://shaeod.tistory.com/574
https://www.youtube.com/watch?v=GLt-D7w8hcg&list=PLDV-cCQnUlIZCF2lTD8GitKEHSqBbi69E&index=3
'Language & etc > C++' 카테고리의 다른 글
const_cast에 대해서 (0) | 2023.02.07 |
---|---|
Rule of Five? 암시적으로 move 생성자/연산자 생성 (0) | 2022.08.25 |
비트 필드 (0) | 2022.07.27 |
[컴파일/링킹]에서 본 Static Member Variables, function... (0) | 2022.07.02 |
std::Move, perfect forwarding - ① (0) | 2022.06.19 |