[cpp] 버퍼로써 vector가 적합하지 않은 이유

@codemaru · January 25, 2022 · 4 min read

C언어에서 동적 버퍼를 사용하던 코드를 C++로 포팅할 때 흔히 vector를 사용한다. 가장 편리하기 때문이다. 하지만 반복적으로 호출되는 코드일 경우 의외로 해당 코드 때문에 부하가 발생할 수 있다. 왜 그런지 살펴보고 더 적합한 C++ 객체가 없는지도 알아보자.

고정 버퍼

dir에 name을 붙여서 파일을 생성하는 흔한 C언어 코드다. 흔히들 이렇게 작성하기도 한다. 하지만 MAX_PATH가 왠지 걸린다. MAX_PATH면 충분할까? 제한도 없어진다는데? 더 긴 경로가 오면 어떻게 될까, 하는 불안감이 든다.

void CreateTempFile(const char* dir, const char* name)
{
	char path[MAX_PATH];

	StringCbCopyA(path, sizeof(path), dir);
	StringCbCatA(path, sizeof(path), name);

	CreateFileA(path, ...);
}

동적 버퍼

길이를 런타임에 계산해서 필요한 만큼 공간을 확보하도록 만들 수 있다. 아래 코드는 dir과 name에 어떤 길이의 문자열이 오더라도 잘 동작한다. 메모리만 있다면 말이다.

void CreateTempFile(const char* dir, const char *name)
{
	char *path;
	size_t size;
	
	size = strlen(dir) + strlen(name) + 1;
	path = malloc(size);
	if(!path)
	    return;
	
	StringCbCopyA(path, size, dir);
	StringCbCatA(path, size name);
	
	CreateFileA(path, ...);
	
	free(path);
}

vector

동적 버퍼를 요하는 코드를 C++로 바꿀 때 흔히 사용하는 객체가 벡터다. 간단하고 깔끔하고 사이즈 관리되고, 나무랄 때가 없는 것이다. 대체하기도 너무 심플하다. resize와 &vector[0]만 사용하면 만사 OK다.

C의 저급함과 C++의 고급함 사이의 가장 적절한 수준을 보여주는 것 같기도 하다. 하지만 한가지 문제가 있다. 이 코드가 그렇게 빠르지는 않다는 점이다. 왜냐하면 벡터는 내부적으로 공간을 확장할 때 모두 초기화를 시키기 때문이다. 사실상 우리는 초기화가 필요 없음에도 resize를 하는 순간 모든 공간이 초기화된다. 낭비다.

void CreateTempFile(const char* dir, const char *name)
{
	std::vector<char> path;
	
	path.resize(strlen(dir) + strlen(name) + 1);
	
	StringCbCopyA(&path[0], path.size(), dir);
	StringCbCatA(&path[0], path.size(), name);
	
	CreateFileA(path.get(), ...);
}

shared_ptr

벡터의 초기화를 피할 수 있는 한가지 대안은 shared_ptr이다. 다만 배열 해제를 위한 메모리 해제 코드를 별도로 작성해야 하는 불편함이 있다. 물론 사이즈도 별도로 트래킹을 해야 한다. 하지만 더이상 벡터가 가지고 있는 개별 아이템 초기화에 대한 부담은 없다. 하지만 또 포인터 소유권과 관련된 레퍼런스 카운트 체크에 대한 부담이 늘어난다.

void CreateTempFile(const char* dir, const char *name)
{
	size_t size;
	
	size = strlen(dir) + strlen(name) + 1;
	std::shared_ptr<char> path(new char[size], [](const char* p) { delete [] p; } );
	
	StringCbCopyA(path.get(), size, dir);
	StringCbCatA(path.get(), size, name);
	
	CreateFileA(path.get(), ...);
}

unique_ptr

또 다른 대안으로 unique_ptr을 사용할 수 있다. unique_ptr<[]>은 특수화가 되어 있어 사용하기도 무척 편리하며 vector와 같이 초기화 코드가 없기 때문에 빠르다. 게다가 레퍼런스 카운트 체크와 관련한 부하도 없기 때문에 더 빠르다. 지역 버퍼 용도로 쓰기에 가장 문안한 형태로 볼 수 있다.

void CreateTempFile(const char *dir, const char *name)
{
	size_t size;

	size = strlen(dir) + strlen(name) + 1;
	std::unique_ptr<char[]> path = std::make_unique<char[]>(size);

	StringCbCopyA(path.get(), size, dir);
	StringCbCatA(path.get(), size, name);
	
	CreateFileA(path);
}
@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중