본문 바로가기
C++/Effective Modern C++

명시적 형식 선언보다는 auto를 선호하라

by COCO1337 2020. 6. 27.

C++11 부터는 auto 문법 덕분에 이 문제점들이 모두 사라졌다. auto 변수의 형식은 해당 초기치로부터 연역되므로, 반드시 초기치를 제공해야한다.

int x1;		// 문맥에 따라서 초기화 되지 않을 수 있음
auto x2;	// 오류 : 초기치가 꼭 필요함
auto x3 = 0;	// 양호함 : x3의 값이 정의됨

그리고 auto는 형식 연역을 사용하므로 컴파일러에만 있던 형식을 지정할 수 있다.

auto derefUPLess =
	[](const std::unique_ptr<Widget>& p1,	// std::unique_ptr들이 가리키는 Widget
	   const std::unique_ptr<Widget>& p2)	// 객체들을 비교하는 함수
	{ return *p1 < *p2; };						
    
auto derefLess =
	[](const auto& p1,			// C++14, 포인터처럼 작동하는 것들이
	   const auto& p2)			// 가리키는 값들을 비교하는 함수
	{ return *p1 < *p2; };

std::function은 C++11 표준 라이브러리의 한 템플릿으로, 함수 포인터 개념을 일반화 한 것이다. 단, 함수 포인터는 함수만 가리킬 수 있지만 std::function은 호출 가능한 객체이면 그 어떤 것도 가리킬 수 있다.

예를 들어 함수 서명에 해당하는 임의의 호출 가능 객체를 지정하는 std::function을 생성한다고 하자.

bool(const std::unique_ptr<Widget>&,		// C++11 버전 std::unique_ptr<Widget> 비교 함수
	const std::unique_ptr<Widget>&)
    
std::function<bool(const std::unique_ptr<Widget>&,
					const std::unique_ptr<Widget>&)> func;
                    
derefUPLess = [](const std::unique_ptr<Widget>& p1,
				const std::unique_ptr<Widget>& p2)
				{ return *p1 < *p2; };

auto와 std::function에서 외견상의 차이보다 더 중요한 차이가 있다.

auto로 선언된, 그리고 클로저를 담는 변수는 그 클로저와 같은 형식이며 그 클로저에 요구되는 만큼의 메모리만 사용한다. 하지만 std::function으로 선언된 변수의 형식은 std::function 템플릿의 한 인스턴스이며, 요구된 클로저를 저장하기에 부족할 수도 있고 그러면 힙 메모리를 할당해 클로저를 저장한다. 그렇기 때문에 std::function 객체는 auto로 선언된 객체보다 메모리를 더 많이 소비한다. 그리고 인라인화를 제한하고 간접 함수 호출을 산출하는 구현 세부사항 때문에, std::function 객체를 통해서 클로저를 호출하는 것은 거의 항상 auto로 선언된 객체를 통해 호출하는 것보다 느리다.


auto의 또 다른 장점은 type shourtcut이라고 부르는 것과 관련된 문제를 피할 수 있다는 것이다.

std::vector<int> v;
unsigned sz = v.size();

auto sz = v.size();		// sz의 형식은 std::vector<int>::size_type

v.size()의 공식적인 반환 형식은 std::vector<int>::size_type이다. Windows 32비트에서는 unsigned와 std::vector<int>::size_type은 같은 크기이지만 64비트 Windows에서는 unsigned가 32비트인 반면 std::vector<int>::size_type은 64비트이다. 하지만 auto를 사용하면 그 혼동을 줄일 수 있다.

 

또 하나의 예로

std::unordered_map<std::string, int> m;

for (const std::pair<std::string, int>& p : m)
{...}

std::unordered_map의 키부분이 const이기 때문에 std::pair의 형식은 std::pair<const std::string, int>이다. 그런데 이 형식은 루프문에 선언된 형식과 다르다. 그렇기 때문에 컴파일러는 std::pair<const std::string, int>를 std::pair<std::string, int>인 p에 해당하는 객체로 변환하려 한다. 각 반복에서 컴파일러는 p를 묶고자 하는 형식의 임시 객체를 생성하고 m의 각 객체를 복사하고 참조 p를 그 임시객체의 묶는 작업을 하게된다.

하지만 이런 문제 또한 auto를 사용함으로써 해결할 수 있다.

for (const auto& p: m)
{...}

게다가 p의 주소를 취하면 실제 m 안의 요소를 가리키는 포인터를 얻게 된다. auto를 사용하지 않은 코드에서는 임시 객체를 가리키는 포인터를 얻게 된다.

 


Conclusion

- auto 변수는 반드시 초기화 해야 하며, 이식성 또는 효율성 문제를 유발할 수 있는 형식 불일치가 발생하는 경우가 거의 없으며, 대체로 변수의 형식을 명시적으로 지정할 때 보다 경제적이다.

- auto로 형식을 지정한 변수는 연역 방식 때문에 문제를 겪을 수 있다.


Reference

Effective Modern C++ 항목 5: 명시적 형식 선언보다는 auto를 선호하라

반응형

댓글