auto in C++

C++ auto 키워드에 대해서
Effective Modern C++ Chapter 2.


auto vs implicit type

변수 초기화

auto는 변수의 선언과 동시에 초기화를 강제할 수 있다.

1
2
3
4
int x;      // 선언되었지만 초기화되지 않음.

auto x; // error!
auto x = 0; // OK. 선언과 동시에 초기화 됨

선언 단순화

autoiterator같이 복잡한 형식의 선언을 단순화 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
template <typename It>
void DoSomething(It b, It e)
{
for(; b != e; ++b)
{
// 명시적으로 iterator가 가리키는 값의 형식을 표현한 경우
typename std::iterator_traits<It>::value_type value = *b;
// auto를 사용하여 iterator가 가리키는 값의 형식을 표현한 경우
auto value = *b;
...
}
}

Closure 형식 사용

C++에서 closure [1]의 형식은 컴파일러만 알고있기 때문에 명시적으로 선언이 불가능하다. 하지만 auto를 사용하면 closure형식을 선언할 수 있다.

1
2
3
4
auto derefUPLess =                      // derefUPLess는
[](const std::unique_ptr<Widget>& p1, // std::unique_ptr이 가리키는
const std::unique_ptr<Widget>& p2) // Widget객체들을 비교하는 클로저의 복사본
{ return *p1 < *p2; }

C++14부터는 람다 표현식의 매개변수에도 auto를 사용할 수 있다.

1
2
3
4
auto derefUpLess = 
[](const auto& p1,
const auto& p2)
{ return *p1 < *p2; }

이제 closure의 복사본인 derefUpLess는 포인터처럼 동작하는 어떤것이든 받을 수 있다.


'왜 굳이 auto를 이용해서 closure의 복사본을 담는 변수를 선언하는가? std::function객체를 이용해도 되지 않을까?'라는 생각이 들수도 있지만 std::functionauto에 비해 몇 가지 단점이 존재한다.
std::function은 C++11에 도입된 함수 포인터 개념을 일반화 한 템플릿이다. 이 템플릿은 함수 포인터 뿐만 아니라 호출 가능한 객체까지 담을 수 있다. std::function을 선언할 때는 함수포인터와 마찬가지로 함수의 서명(Signature)을 지정해야 한다.

1
2
std::function<bool(const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)> func;

closure역시 호출 가능한 객체이므로 std::function에 그 복사본을 담을 수 있다.

1
2
3
4
5
std::function<bool(const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)>
derefUPLess = [](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; };

한눈에 보기에도 선언 과정에서 std::functionauto를 사용한 closure변수보다 복잡하다. 하지만 눈에 보이지 않는 더 큰 단점이 존재한다.

auto를 사용해서 closure 형식의 변수를 선언하면, 그 closure가 사용하는 만큼의 메모리만 사용된다. 그러나 std::function을 사용하여 closure의 복사본을 담는 변수를 선언하면 그 형식은 std::function템플릿의 한 인스턴스이며, 그 크기역시 std::function에 implemetaion에 의해 결정된다. 결과적으로 대부분 closure 객체보다 더 많은 메모리를 사용한다.
그리고 함수의 인라인화를 제한하고 함수 호출 오버헤드때문에, std::function 객체를 통해서 closure를 호출하는 것은 auto로 선언된 객체를 통해 호출하는 것보다 대부분 느리다.


형식 불일치 예방

auto의 또다른 장점은 '형식 불일치’를 예방할 수 있다는 점이다.

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

위 코드에서 v.size()의 반환 형식은 std::vector<int>::size_type이다. 플랫폼에 따라서 unsignedstd::vector<int>::size_type의 크기가 같을수도, 다를수도 있다. 예를 들어 32비트 Windows에서는 전자는 32비트, 후자는 64비트의 크기를 갖는다. 이때 위 코드는 32비트 변수에 64비트 값을 할당한다는 문제가 발생한다.

auto를 사용한다면 이런 문제를 피할 수 있다.

1
auto sz = v.size(); // sz의 형식은 항상 std::vector<int>::size_type과 같다.

std::unordered_map같은 해시 테이블을 사용할 때에도 auto는 실수를 줄여준다.

1
2
3
4
5
6
7
std::unordered_map<std::string, int> m;
...

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

위 코드에서 해시 테이블에 담긴 std::pair의 형식은 std::pair<const std::string, int>이다. 하지만 루프의 변수 p의 형식은 std::pair<std::string, int>로 선언되어있다.
따라서 내부적으로 루프를 돌때마다 해시 테이블에 담긴 객체들을 p의 형식에 맞춰서 임시 객체를 만들고, 객체를 임시객체로 복사한 뒤, p가 그 임시객체를 참조하게한다. 그리고 루프의 끝에서 이 임시 객체를 파괴한다.

이처럼 실수에의해 의도하지 않은 형식 불일치를 auto를 통해 사전에 방지할 수 있다.

1
2
3
4
for(const auto& p : m)
{
...
}

auto with implicit type

auto는 가끔 우리눈에 보이지 않는 형식으로 추론되는 경우가 있다. 대표적인 예는 ={}로 추론되는 std::initialize_list<T>가 있다. 또 주의해야 하는경우는 Proxy pattern 이 사용되는 경우이다.
std::vector<bool>operator[]bool&형식이 아닌 내부적으로 선언된 프록시 클래스 std::vector<bool>::reference형식을 반환한다. 이 반환값을 변수에 저장할 때 명시적으로 bool형식 변수를 선언하는 경우에는 상관 없지만[2], auto를 사용하면 의도하지 않은 형식의 변수가 선언된다.

1
2
3
std::vector<bool> v;
bool b = v[5]; // std::vector<bool>::reference가 묵시적으로 bool로 변환
auto b = v[5]; // b의 형식은 std::vector<bool>::reference

이처럼 auto가 우리의 의도와는 다르게 추론되는 경우 auto의 형식 추론을 강제하는 방법을 사용해야 한다.

1
auto b = static_cast<bool>(v[5]);

위 코드에서 auto는 명시적 캐스팅에 의해서 bool형식을 추론하도록 강제된다.


캐스팅을 통해 auto의 추론을 강제하는 방식은 코드에 개발자의 의도를 담을 수 있다는 장점도 있다.

1
double calcEpsilon();

calcEpsilon함수는 double의 정밀도를 갖는 값을 반환한다.

만약 개발자가 반환 값을 float의 정밀도로 낮춰서 사용하고 싶다면 float변수에 그 값을 담으면 된다.

1
float ep = calcEpsilon();

하지만 이 코드는 'calcEpsilon함수 반환값의 정밀도를 일부러 낮추려고 한다’라는 의도를 명확하게 나타내기 힘들다.

반면 auto의 추론을 강제하는 방식을 사용하면 이 의도를 명확하게 나타낼 수 있다.

1
auto ep = static_cast<float>(calcEpsilon());

References


  1. C++의 closure는 lambda 표현식에 의해서 런타임에 만들어진 객체이다. ↩︎

  2. std::vector::reference가 묵시적으로 bool로 형변환 된다. ↩︎

Author

Joyus.Gim

Posted on

2022-06-13

Updated on

2022-07-19

Licensed under

Comments