[Effective Modern C++] 6. auto가 원치 않은 타입으로 추론 될 때에는 명시적 타입의 초기화를 생각하자
728x90
반응형
일단, 보이지 않는 프록시 타입 때문에 auto가 초기화 표현식의 타입을 잘못 추론할 수 있다는 점을 알아야한다.
예를 들어, std::vector<bool>::operator[]의 반환 타입은 bool&이 아니라 어떤 프록시 클래스 타입이다.
프록시 클래스 중에는 사용자에게 명백히 드러나도록 설계된 것들도 있지만(std::shared_ptr, std::unique_ptr) 보통 보이지 않도록 숨겨져 있다.
그렇기 때문에 보통 프록시 클래스는 auto와 잘 맞지 않는다. 때문에 다음과 같은 코드는 미정의 동작을 유발한다.
// Widget을 하나 받고 std::vector<bool>을 돌려주는 다음과 같은 함수가 하나 있다고 하자.
std::vector<bool> features(const Widget& w);
// 해당 bool 값들은 그 Widget이 특정 기능(feature)을 지원하는지를 뜻한다고 가정한다.
// 더 나아가서, 5번 비트는 주어진 Widget의 우선순위가 높은지를 나타낸다고 가정하자.
// 이 함수를 이용해서 다음과 같은 코드를 작성할 수 있을 것이다.
Widget w;
...
bool highPriority = features(w)[5]; // w의 우선순위가 높은가?
...
processWidget(w, highPriority); // w를 그 우선순위에 맞게 처리한다.
// 이 코드는 딱히 문제될 것이 없다. 하지만 bool 대신 auto로 highPriority를 선언하면?
auto highPriority = features(w)[5]; // w의 우선순위가 높은가?
// 여전히 컴파일되지만, 이제는 그 행동을 예측할 수 없다.
processWidget(w, highPriority); // 미정의 행동!
// 그 이유는 std::vector<bool>::operator[]가 bool&을 반환하는 것이 아니라, std::vector<bool>::reference라는 프록시 객체를 반환하고, 이러한 객체의 수명이 한 문장 이상으로 연장되지 않도록 만들어지는 것이 보통이기 때문이다.
// 즉, processWidget이 호출될 시점에는 이미 highPriority 객체(std::vector<bool>::reference 객체) 내부의 포인터는 이미 소멸된 대상을 가리키고 있게 된다.
// 정리하자면, 다음과 같은 형태의 코드는 피해야 한다.
auto someVar = "보이지 않는" 프록시 클래스 타입의 표현식;
보이지 않는 프록시 클래스는 일상적인 용법에서는 그 존재가 드러나지 않도록 설계된 것이지만, 그래도 라이브러리의 문서에는 프록시 클래스의 존재가 명시되어 있는 경우가 많다.
또한 문서화의 결함을 헤더 파일이 채워주기도 한다. 소스 코드에서 프록시 객체의 존재를 완전이 숨길 수 있는 경우는 거의 없다.
일반적으로 프록시 객체는 사용자가 호출하도록 만들어진 어떤 함수가 돌려주며, 그런 함수의 시그니처를 보면 프록시 객체의 존재를 확인할 수 있는 경우가 많다.
// std::vector<bool>::operator[]의 명세이다.
namespace std
{
template <class Allocator>
class vector<bool, Allocator>
{
public:
...
class reference { ... };
reference operator[](size_type n);
...
};
}
// 보통의 경우 std::vector<T>의 operator[]가 T&를 돌려준다는 사실을 알고 있다면 이 operator[]의 반환 타입을 생소하게 느낄 것이며, 그러한 생소함은 곧 이 부분에서 프록시 클래스가 쓰이고 있음을 암시해주는 힌트라 할 수 있다.
타입 명시 초기치 관용구는 초기화에 쓰이는 표현식이 산출하는 타입과는 다른 타입으로 변수를 생성하고자 하는 의도를 명확하게 나타내는 데도 유용하다.
아까 문제 되었던 코드를 다시 보자.
std::vector<bool> features(const Widget& w);
Widget w;
...
auto highPriority = features(w)[5];
// features의 반환 타입은 std::vector<bool>이고 std::vector<bool>::operator[]의 반환 타입은 std::vector<bool>::reference인데, 우리가 필요한 것은 bool 타입이다.
// 다음은 highPriority가 bool로 추론되도록 강제하는 예이다.
auto highPriority = static_cast<bool>(features(w)[5]);
---
// 타입 명시 초기치 관용구는 초기화에 쓰이는 표현식이 산출하는 타입과는 다른 타입으로 변수를 생성 하고자하는 의도를 명확하게 나타내는 데에도 유용하다.
// 예를 들어 어떤 허용 한계치를 계산하는 다음과 같은 함수가 있다고 하자.
double calcEpsilon(void); // 허용 한계치를 돌려준다.
// 그런데 float의 정밀도로 충분하며, double이 float에 비해 크다는 점이 걱정이라고 하자.
// 그럼 calcEpsilon의 결과를 float에 담으면 된다.
float ep = calcEpsilon(); // double에서 float로의 암묵적 변환이 일어남
// 그런데 이 코드는 "함수가 돌려준 값의 정밀도를 일부러 줄이고자 한다"는 의도를 명확하게 표현하지 않는다.
// 타입 명시 초기치 관용구를 이용하면 그런 의도가 표현된다.
auto ep = static_cast<float>(calcEpsilon());
위처럼 타입 명시 초기치 관용구를 이용하면 의도를 명확하게 나타낼 수 있다.
요약
- 보이지 않는 프록시 타입 때문에 auto가 초기화 표현식의 타입을 잘못 추론할 수 있다.
- 타입 명시 초기치 관용구는 auto가 원하는 타입을 추론하도록 강제한다.
728x90
반응형
'Books > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 8. 0과 NULL보다 nullptr을 사용하자 (0) | 2022.08.28 |
---|---|
[Effective Modern C++] 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하자 (0) | 2022.08.28 |
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자 (0) | 2022.08.15 |
[Effective Modern C++] 4. 추론된 타입을 파악하는 방법을 알아두자 (0) | 2022.08.06 |
[Effective Modern C++] 3. decltype의 작동 방식을 숙지하자 (0) | 2022.08.06 |
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 8. 0과 NULL보다 nullptr을 사용하자
[Effective Modern C++] 8. 0과 NULL보다 nullptr을 사용하자
2022.08.28 -
[Effective Modern C++] 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하자
[Effective Modern C++] 7. 객체 생성 시 괄호(())와 중괄호({})를 구분하자
2022.08.28 -
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자
2022.08.15 -
[Effective Modern C++] 4. 추론된 타입을 파악하는 방법을 알아두자
[Effective Modern C++] 4. 추론된 타입을 파악하는 방법을 알아두자
2022.08.06