Little Jay

[C++] emplace_back()과 push_back(), 그리고 Clang-Tidy Bug 본문

Univ/Study

[C++] emplace_back()과 push_back(), 그리고 Clang-Tidy Bug

Jay, Lee 2022. 2. 16. 17:40

vector container을 사용할 때 대부분 push_back()을 사용한다.

직관적으로 vector 컨테이너에 원소를 위에 삽입한다라는 의미이다.

 

그러나 push_back()함수에는 문제점이 존재한다. 

push_back() 함수는 '객체'를 집어넣는 형태이므로, 원형은 아래와 같다.

void push_back (const value_type& val);
void push_back (value_type&& val);

emplace_back()도 동일하게 컨테이너 마지막에 '객체'를 삽입하는 기능을 수행한다.

 

push_back()쪽을 먼저 본다면 push_back()은 rvalue, 즉 임시객체가 필요하다.

그렇기 때문에 push_back()이 어떻게 동작을 하나면, 인자로 필요한 객체를 생성 후  push_back() 함수 내부에서 다시 한번 복사가 일어난 뒤 push_back이 끝날 때 인자들과 객체가 파괴된다.

즉 객체를 하나 추가할 때 쓸데없이 2번 복사하고 파괴한다.

 

emplace_back 함수는 가변인자 템플릿을 사용하여 객체 생성에 필요한 인자만 받은 후,

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

원형은 아래와 같다.

template< class... Args >                 			//(since C++11)
void emplace_back( Args&&... args );      			//(until C++17)
template< class... Args >                 			//(since C++17)
reference emplace_back( Args&&... args );			//(until C++20)
template< class... Args >
constexpr reference emplace_back( Args&&... args );	//(since C++20)

push_back과 같은 삽입 함수들은 삽입할 객체를 받지만,

emplace_back과 같은 생성 삽입 함수는 삽입할 객체의 생성자를 위한 인자들을 받아

std::vector 내에서 직접 객체를 생성하여 삽입하므로 임시 객체의 생성과 파괴, 복사(혹은 move)를 하지 않아도 되어

성능상 더 유리 할 수 있다.

 

(코드 출처: https://openmynotepad.tistory.com/10)

#include <iostream>
#include <vector>
#define endl '\n'
using namespace std;

class myVec {
public:
	myVec(const int _n) : m_nx(_n) { cout << "일반 생성자 호출" << endl; }
	myVec(const myVec& rhs) : m_nx(rhs.m_nx) { cout << "복사 생성자 호출" << endl; }
	myVec(const myVec&& rhs) : m_nx(std::move(rhs.m_nx)) { cout << "이동 생성자 호출" << endl; }
	~myVec() { cout << "소멸자 호출" << endl; }
private:
	int m_nx;
};

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

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

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

	return 0;
}

위의 코드를 push_back()과 emplace_back()을 각각 주석 처리해서 실행시켜보면 

push_back()에서는 이동 생성자가 호출되었고, 소멸자도 총 두번 실행된다.

반면에 emplace_back() 같은 경우에는 이동 생성자가 호출되지 않으며, 소멸자도 한번 실행 된다.

 

이래서 얼핏 보면 emplace_back()이 push_back()보다 성능이 좋으니까 이걸 써야겠다고 생각할 수 있다.

물론 emplace_back()을 사용하지 말라는 것이 아니다.

분명 이 함수도 단점이 존재하기에 이 함수의 동작 원리를 알고 사용하면 좋을 것 같다. 

 

먼저 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
}

explict 명령어로 형변환을 막았음에도 불구하고 emplace_back()은 이를 무시하고 그냥 형변환을 진행시켜버린다.

이 문제점은 살짝 critical할 수 있는데, 컴파일러가 이를 컴파일 시에 에러를 못잡아 준다는 것이다.

이것이 우리가 의도한 것이 아니라면 명시적인 push_back()을 고려하는 것이 더 좋을 것이다.

 

또한 다른 문제점은 앞에서 봤던 소멸자가 없다는 것이다.

emplace_back()의 인자로 포인터 형식이 들어왔으면 생성자는 호출이 되지만 소멸자가 호출되지는 않아 memory를 delete하지 않을 수 있다. 이는 당연하게도 memory leak와도 연결된다. 

 

정리하자면 emplace_back()은 안전하지 않은 코드이다.

성능적으로는 push_back()보다는 좋지만 이러한 리스크들을 안고 emplace_back()을 사용하기에는 부담이 되는 것은 부정할 수 없는 사실인 것 같다. 

 

내가 emplace_back()을 사용하는 경우는 거의 백준이나 프로그래머스에서 vector 컨테이너를 사용할 때 주로 사용하고 나머지는 거의 push_back을 사용하는 경향이다. (이거 때문에 자료구조 실습 수업 할때 한번 데인적이 있다)

그래서 emplace_back()은 양날의 검임을 알고 조심히 사용하자.

 

이 글을 쓴 이유는 Clion IDE의 Clang-Tidy의 버그가 존재하기 때문이다. 

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;

template<typename T>
vector<T> merge(vector<T>& arr1, vector<T>& arr2) {
    vector<T> merged;

    auto iter1 = arr1.begin();
    auto iter2 = arr2.begin();

    while(iter1 != arr1.end() && iter2 != arr2.end()) {
        if (*iter1 < *iter2) {
            merged.template emplace_back(*iter1);
            iter1++;
        }
        else {
            merged.template emplace_back(*iter2);
            iter2++;
        }
    }
    if (iter1 != arr1.end()) {
        for (; iter1 != arr1.end(); iter1++)
            merged.template emplace_back(*iter1);
    }
    else {
        for (; iter2 != arr2.end(); iter2++)
            merged.template emplace_back(*iter2);
    }
    return merged;
}

template<typename T>
vector<T> merge_sort(vector<T> arr) {
    if (arr.size() > 1) {
        auto mid = size_t(arr.size() / 2);
        auto left = merge_sort<T>(vector<T>(arr.begin(), arr.begin() + mid));
        auto right = merge_sort<T>(vector<T>(arr.begin() + mid, arr.end()));
        return merge<T>(left, right);
    }
    return arr;
}

병합정렬(merge sort)를 구현한 코드이다. 

이 코드를 보게 되면 이상한 것이 있는데 바로 emplace_back()을 하는 부분이다.

일반적으로 벡터의 삽입 함수는 vector.push_back(), vector.emplace_back() 을 하면 정상적으로 동작한다.

그러나 CLion의 Clang-tidy는 내가 emplace_back()을 할 때 코드 자동완성을 저렇게 시켜주었다.

심지어 저렇게 이상한 코드도 잘 동작을 한다. 

위의 코드를 repl.it 에서도 돌려보았지만 문제없이 컴파일 되었다.

Clang을 사용하는 IDE에서는 이 코드들이 정상적으로 돌아간다. 

 

이 부분이 이상하게 여겨 stackoverflow에 질문을 했고 돌아온 답변은 CLion의 Clang-tidy에 버그가 있다는 답변이었다. 

https://stackoverflow.com/questions/71124743/c-template-keyword-before-emplace-back-how-does-it-works/71125686#71125686

 

C++, 'template' keyword before emplace_back(), how does it works?

I was studying Algorithm in C++ using CLion. I'm currently using Windows 10, Clion 2021.3.3 with bundled MingW I was working on code, Merge Sort algorithm, and I had chance to use vector.emplace_ba...

stackoverflow.com

 

원래 vector.template emplace_back<>()은 허용이 된다. 

내가 정의한 벡터와 emplace_back()이 가변인자 템플릿을 사용하기 때문에

여기에 자료형을 명시적 혹은 암시적으로 써줘도 되기 때문이다.

그러나 일부 컴파일러들은 이 규칙을 적용하는 것이 매우 느슨하기 때문에

템플릿 접두사가 있는 버전도 받아들이지만 이는 명백히 기술적으로 잘못된 것이다.

혹시나 CLion을 쓰다가 나처럼 에러가 발생한 사람들이 궁금해 할까봐 글을 남기게 되었다.

 

Reference

https://openmynotepad.tistory.com/10

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

https://cypsw.tistory.com/33

https://sonagi87174.tistory.com/14

https://blog.naver.com/sorkelf/220825930008

 

Comments