auto 키워드
C++ 11에 추가된 기능으로 함수 반환 값이나 초기화시 데이터 타입을 자동으로 확인하여(연역하여) 변수 타입을 지정해준다.
auto 키워드의 예시
auto x = 5; // int
auto f = 1.0f // float
auto d = 5.0 // double
int func() {return 0;}
auto res = func(); // 함수 결과에 대한 타입 연역
auto foo = func; // 함수 포인터
auto reset = [](Type* ptr) { ptr->reset(); } // 람다 클로저 함수
타입 연역을 위해 반드시 초기화값이 필요한 것이 특징이다.
타입을 직접 타자로 칠 때, 타입명이 길다면 불편한 점이 있는데 이를 자동으로 지정해주는 편함이 있다.
반복자의 실제 변수 타입을 지정하는 타입명이 긴 경우
예시) Iterator의 value_type
template<typename Iter>
void trim(Iter begin, Iter end)
{
for(; begin != end; ++begin)
{
typename std::iterator<Iter>::value_type
currValue = *begin;
}
}
람다 객체에 자주 사용하는 auto 키워드
vector<Marker*> markers;
// class lambda[] bool(Marker*, Marker) -> bool
auto equal = [](Marker* a, Marker* b)
{
return a == b;
};
// C++ 14에서 람다 표현식의 매개변수에 auto를 지정할 수 있음
auto equal_14 = [](const auto& a, const auto& b)
{
return a == b;
};
auto 키워드 vs std::function을 이용한 람다 객체를 담기
람다 객체는 호출 가능하므로 std::function에 할당 가능하다.
람다 객체는 클로저가 캡쳐하는 변수 종류와 수 그리고 레퍼런스 혹은 값 타입으로 캡쳐하는 지에 따라 요구되는 메모리가 가변적이다.
std::function<T>은 템플릿 인수 T에 상관없이 고정된 크기를 가지게된다.
만약 function 객체가 클로저를 고정된 메모리에 담을 수 없는 경우 힙 메모리에 동적 할당하여 클로저를 저장한다.
// 람다 객체 1
auto equal = [](Marker* a, Marker* b)
{
return a == b;
};
// function 객체 1
std::function<bool(Marker*, Marker*)>
equal_func = [](Marker* a, Marker* b)
{
return a == b;
};
int p = 5;
// 람다 객체 2
auto less = [p](Marker* a, Marker* b)
{
return a->mark < p&& b->mark < p;
};
// function 객체 2
std::function<bool(Marker*, Marker*)> less_func = [p](Marker* a, Marker* b)
{
int p = 5;
return a->mark < 5 && b->mark < 5;
};
printf("람다 객체 사이즈 : %lld, function 객체 사이즈 %lld\n", sizeof(equal), sizeof(equal_func));
printf("람다 객체2 사이즈 : %lld, function 객체2 사이즈 %lld\n", sizeof(less), sizeof(less_func));
따라서 대체적으로 function 객체 메모리가 더 큰 고정 크기를 가지므로 메모리 소비량이 더 많다.
또한 function 객체는 인라인화를 제한하고 간접 호출을 사용하는 구현 때문에 auto로 선언된 람다 객체를 호출하는 것보다 느리다. 따라서 클로저 함수는 auto 키워드를 사용하여 초기화하는 것이 일반적이다.
- 유지 보수 측면
원래 함수의 반환값을 수정하였을 때, auto 키워드를 사용하였다면, 해당 함수와 관련된 모든 반환값에 대하여 타입을 수정할 필요가 없다.
※ 다른 1 워드 크기를 가진 운영체제에 따라 함수의 반환값이 다를 수 있다.
vector 컨테이너의 size() 함수는 vector<T>::size_type을 반환하는데 32비트 운영체제에서는 4 바이트의 부호없는 정수가 반환되지만 64비트 운영체제에서는 8 바이트의 부호없는 정수가 반환된다. 서로 다른 운영체제에서 프로그램을 이식할 때 다른 타입에 대한 유지 보수가 필요하다.
- 사용자가 의도하지 않는 타입 지정으로 발생한 비용 혹은 문제를 방지
std::unordered_map<std::string, int> m;
const auto& pair = *m.begin(); // const std::pair<const std::string, int>
// 의도한 타입은 const std::pair<const std::string, int>
for (const std::pair<std::string, int>& p : m)
{
// 관련 코드 실행
}
위 코드에서 해쉬 테이블(unordered_map)의 데이터 타입은 pair<string, int>이다. 만약 사용자가 반복자를 이용하여 모든 원소에 대해 const로 데이터 값을 접근하고자 하는 의도로 for 문이 작성되었다고 하자.
올바른 타입은const pair<const string, int>이지만 const를 실수로 누락하였고 매 루프 마다 해당 p 변수에 대해 변환이 이루어 진다.
즉, 다음과 같은 비용이 추가적으로 발생한다.
const pair<const string, int> 임시 객체 = 실제 컨테이너 원소 // 복사
const pair<const string, int> &p = const pair<const string, int> 임시 객체 // 참조 (묶음)
p 변수는 매 루프의 끝에 파괴되고 다시 할당된다.
auto 키워드를 사용하면 위와 같이 의도하지않는 비용 이나 문제를 방지할수 있다.
for (const auto& p : m)
{
// 관련 코드 실행
}
p는 실제 컨테이너의 원소를 참조하는 변수로 묶어진다.
- auto 키워드에 대한 고찰
auto 키워드는 실제 타입을 알기 어렵기 때문에 가독성 문제가 있을 수 있다. 하지만 이는 현대 컴파일러 기능를 포함한 IDE에서 어느 정도 auto에 대한 연역 결과를 제공하며, 어떤 타입의 컨테이너 인지, 스마트 포인터 인지 등, 추상적으로 파악하는 것이 코드가 복잡하지 않고 오히려 개발을 도와줄 수 도 있다. 그렇다고 auto를 반드시 쓰는 게 좋다는 것이 실무에서 증명된 것은 아니므로 적절한 판단으로 auto를 활용하는 것이 좋다.
원치 않는 형식으로 연역될 때가 있는데 이때는 명시적 형식을 이용해야한다.
vector<bool> 컨테이너의 operator[] 반환값에 대한 특이한 타입
vector<bool> 컨테이너는 원소 접근자 operator[]에 대해 일반적인 레퍼런스 타입 T&를 반환하지 않는다.
vector<bool> features = vector<bool>(16, false);
auto ib = features[0];
실제 연역된 auto의 타입은 std::vector<bool>::reference 타입이다.
vector<bool> 컨테이너는 bool 원소를 1 비트 크기의 압축된 형태로 저장하고 있다. C++에서 비트 단위의 참조는 금지되고 있으므로 (최소 변수 크기도 1바이트이다.) 실제 원소에 대한 반환을 1 비트로 할 수가 없다. 다른 일반적인 타입 int, float, double과 같이 T&로 반환하기 위해 "bool&"를 흉내낸 포장지와 같은 클래스를 반환하는데 이 클래스가 std::vector<bool>::reference인 것이다.
std::vector<bool>::reference 구현 방식에 따라 다르지만 가장 쉽게 생각해 보면, 전체 데이터 메모리를 1 워드 크기로 자르고 각 1 워드 단위를 가리키는 포인터와 1 워드 내에서 몇 번째(오프셋) 위치인지 기록한다면, 모든 1 비트 단위의 데이터를 접근할 수 있다.
이러한 실제 반환된 값을 대신하는 클래스를 대리자(proxy)라고 하며 소프트웨어 설계 패턴에서std::vector<bool>::reference과 같이 사용자에게 보이지 않도록 설계되는 대리자 클래스를 자주 사용하기도 한다.
vector<bool> GetPermission();
void authenticate(bool);
enum class USER {
USER_A = 0,
USER_B,
USER_C,
USER_END,
};
int main()
{
auto okay = GetPermission()[(int)USER::USER_A];
// GerPermission()의 함수 반환값에 대한 임시 객체 삭제로
// okay는 dangling pointer
authenticate(okay);
return 0;
}
vector<bool> GetPermission()
{
int user_num = (int)USER::USER_END;
vector<bool> auth(user_num, true);
auth[0] = true;
return auth;
}
void authenticate(bool b)
{
// if authorized
if (b)
{
}
}
위 예시 코드를 보면, std::vector<bool>::reference를 auto로 타입 연역하여 okay 변수를 선언하였다.
okay 변수는 실제 UserA에 대한 권한 비트가 담겨 있지 않고 std::vector<bool>::reference 타입의 어떤 포인터가 담겨 있을 것이다.
int main()
{
auto okay = GetPermission()[(int)USER::USER_A];
// GerPermission()의 함수 반환값에 대한 임시 객체 삭제로
// okay는 dangling pointer
authenticate(okay);
return 0;
}
문제는, GetPermission() 함수의 결과로 반환된 임시 컨테이너 객체가 문장 끝에서 파괴되므로 okay 변수가 가르키고 있던 메모리는 삭제되고 없는 주소를 가르키게 된다. 이는 댕글링 포인터를 만들어내고 authenticate를 호출 시 댕글링 포인터를 bool 타입으로 캐스팅하기 때문에 런타임 오류가 발생하게 된다. 이런 식의 코드를 실제로 짤 일은 거의 없겠지만, 이때는 반드시 숨겨진 대리자 인스턴스를 bool 타입으로 암묵적 변환을 통해 명시적으로 담아두도록한다.
int main()
{
auto okay = static_cast<bool>(GetPermission()[(int)USER::USER_A]);
// 또는 암묵적 변환을 통한 명시적 형식으로 초기화
bool okay = GetPermission()[(int)USER::USER_A];
authenticate(okay);
return 0;
}
※ 다음과 같이 vector<bool>이 이름있는 변수로 남아 있었다면, 오류없이 실행되기는 하다.
int main()
{
vector<bool> permissions = GetPermission();
auto okay = permissions[(int)USER::USER_A];
authenticate(okay);
return 0;
}
대리자 클래스는 auto 키워드와 잘 맞지 않는 편이므로 대리자 클래스 인스턴스를 반환할 경우 명시적 형식을 통한 초기화를 사용하도록 한다.
현대 C++에서는 암묵적 변환을 피하고 사용자의 의도를 명시하는 것이 좋으므로 다음과 같은 static_cast를 이용한 초기화가 바람직하다.
double calculatePhysics() {return 3.141592e-18 / 1.4e-8;};
auto res = static_cast<float>(calculatePhysics());
참고: Effective Modern C++
'Advanced C++' 카테고리의 다른 글
[Modern C++] (3-1) 현대적 C++에 적응하기 (0) | 2021.03.25 |
---|---|
[C++ 클래스] 템플릿 클래스와 전방 선언 (0) | 2021.03.24 |
[Modern C++] (1) 형식 연역 (0) | 2021.03.19 |
[C++] 스마트 포인터 shared_ptr/unique_ptr/weak_ptr (0) | 2021.03.06 |
[C++] 함수 객체 Functor, std::function, std::bind (0) | 2021.02.22 |