C++에선 자료형을 추론하는 기능으로 auto와 decltype을 제공해준다.
auto
std::unordered_map<std::string, std::vector<std::pair<int, char>>> Map;
위와 같이 선언된 unordered_map이 있다고 해보자.
key는 std::string이고 value는 std::vector<std::pair<int, char>> 이다.
이 자료구조를 일반적으로 사용하기 위해선 아래와 같이 코드를 작성해야 한다.
//이터레이터
std::unordered_map<std::string, std::vector<std::pair<int, char>>>::iterator Iter = Map.begin();
//범위기반 for문
for(const std::unordered_map<std::string, std::vector<std::pair<int, char>>>& _Pair : Map)
{
//로직
}
위처럼 map의 구조가 복잡할수록 매번 자료형을 작성하는 것이 매우 복잡하게 된다.
정도가 심해질수록 코드의 작성도 어려워지지만, 가독성을 심각하게 저해하게 된다.
이럴 때, auto를 사용하면 어떻게 될까?
//이터레이터
auto Iter = Map.begin();
//범위기반 for문
for(auto _Pair : Map)
{
//로직
}
이렇게 복잡한 자료형을 auto로 대체할 수 있다. auto 키워드는 컴파일 타임에 적합한 자료형으로 추론하여 컴파일된다.
자료형이 길고 복잡하거나, 코드를 작성할 때 정확한 자료형을 유추하기 힘든 경우(템플릿 등)엔 auto 키워드를 적절히 사용하는 것이 도움이 된다.
하지만, 위에서 작성한 auto를 사용하지 않은 코드와 auto를 사용한 코드는 정확히 일치할까? 답은 아니다.
어디서 그럼 차이가 발생하는걸까?
아래에서 auto로 추론된 자료형을 한 번 보자.
처음에 선언했던 자료형은 const std::unordered_map<std::string, std::vector<std::pair<int, char>>>& 이었다.
하지만, 이를 auto로 바꿔서 컴파일러의 추론에 맡겼더니, 참조형이 사라졌도 const는 key만 붙어있는 형태로 추론하였다.
즉, auto는 const와 참조형은 무시한다는 것을 알 수 있다. (const는 원래 아예 무시되지만, map의 경우 key는 반드시 const 형태로만 사용할 수 있기 때문에 key에만 const가 붙어있는 것이다.)
즉, auto는 항상 의도한대로 자료형을 추론하는 것은 아니라는 것이다.
만약 const를 의도했다면 const auto를 사용해야 하고, 참조형을 의도했다면 const auto&를 사용해야 한다.
또한 volatile 키워드도 무시하기 때문에 volatile 키워드도 신경써서 붙여주어야 한다.
참고로 auto는 초기화 값을 기준으로 자료형을 추론하기 때문에 auto 의 사용은 반드시 초기화와 함께 이루어져야 한다.
//X
auto A;
//O
auto B = 3;
auto C = "ABC";
auto D = Func();
decltype
auto가 초기화 값을 기준으로 자료형을 판단한다면 decltype은 식을 기준으로 자료형을 판단한다.
아래의 코드를 보자.
decltype(2) A;
decltype('C') B;
decltype(Func()) C;
decltype 키워드 뒤에 있는 괄호 속에 있는 식의 최종 결과값을 기준으로 자료형이 추론된다.
식을 기준으로 값의 자료형을 결정하기 때문에, auto처럼 초기화가 반드시 이루어지지 않아도 된다.
(하지만, 초기화는 항상 하는게 좋다. 오류가 나지 않을 뿐 다른 문제는 언제든 발생할 수 있다.)
decltype은 auto와 달리 참조형과 const, volatile까지 그대로 자료형으로 추론해준다.
const int A = 3;
decltype(A) B;
이렇게 선언하면, B는 const int 타입으로 추론된다.
그렇기 때문에, 위의 B는 반드시 초기화를 해주어야 한다. 이는 decltype때문이 아니라, 자료형이 const이기 때문이다.
이처럼, const가 포함된 형태로 자료형이 잘 추론되었기 때문에 초기화를 하지 않으면 에러가 발생하게 된다.
참조형 또한 잘 추론되는 것을 볼 수 있다. 참조형도 const처럼 초기화가 반드시 이루어져야 하므로 에러가 발생한다.
decltype은 괄호 안의 식을 기준으로 자료형을 판별한다.
int A = 3;
decltype(A) B;
decltype((A)) C;
그렇다면 위의 코드에선 자료형이 어떻게 추론될까?
괄호 하나 더 씌운게 뭔 차이라고 당연히 B나 C나 똑같은 int 일 것이라고 생각이 들겠지만, B는 int형이고 C는 int& 형이다.
괄호 하나 더 씌웠다는 이유로 C의 경우 참조형으로 추론이 되는 것이다.
왜 그런 차이가 발생할까?
decltype은 괄호 안에 있는 식을 두 가지의 방식으로 추론한다.
먼저, 괄호가 씌워져 있지 않다면 결과 값의 자료형을 그대로 추론하게 된다.
반면, 괄호가 씌워져 있다면 식의 결과 값이 xvalue인지, lvalue인지, prvalue인지를 고려하여 자료형을 추론하게 된다.
식의 결과 값이 lvalue라면, 참조형(&)으로 추론되며 xvalue라면 이동참조형(&&)으로 추론되고 prvalue라면 값형으로 추론된다. 그런데, lvalue,xvalue, prvalue가 뭔데 이런 차이가 발생하는걸까?
lvalue, xvalue, prvalue가 뭔지 간단하게 설명해보겠다.
lvalue란, 일반적인 변수이다. 프로그래머에 의해 변수 이름이 부여되어 있고, 주소값을 참조할 수 있는 대상이다.
xvalue란, 함수의 반환 등에서 발생하는 임시 객체와 같이 존재는 하지만 의도적으로 주소값을 참조할 수는 없고 변수의 이름이 부여되어 있지는 않은 존재를 의미한다.
prvalue란, 문자열을 제외한 리터럴 상수이다. (1, 2, 3, 'a', 'b', 'c' 등)
흔히 알고 있는 rvalue란 xvalue와 prvalue를 함께 포함하는 개념이며, xvalue는 rvalue 중에서도 이동, 복사 등이 가능한 대상을 포함하는 개념인 것이다.
이 개념을 기준으로 생각해보자.
decltype은 초기화를 할 필요가 없다고 했지만, 일반적으로는 항상 초기화와 함께 이루어질 것이다.
왜냐하면, 어떠한 식의 결과를 저장할 자료형을 스스로 추론할 수 없을 때 이를 컴파일러에 맡기는 형태이기 때문이다.
decltype(Func()) A = Func();
보통은 이렇게, Func()라는 함수의 반환형을 모를 때, 이를 추론하기 위해 decltype을 활용하는 것일 테니 말이다.
그렇다면, 위에서처럼 초기값으로 설정한 함수의 반환값이 lvalue일 때를 생각해보자.
이 값을 만약 decltype이 값형으로 추론해버리면 복사가 발생할 것이다.
그렇기 떄문에 참조형으로 추론하는 것이 불필요한 복사를 막을 수 있어 효율적일 것이다.
그러므로 반환형이 lvalue라면 참조형으로 추론하는 것이 가장 합리적일 것이다.
만약 xvalue라면?
위와 동일하게 생각해보자. xvalue는 곧 사라질 객체인데 굳이 복사할 필요가 있을까 생각해보면 당연히 없다. 가능하다면 이동연산자를 호출하는 것이 훨씬 유리하다. 그러므로 xvalue는 이동참조(&&)로 추론하는 것이 합리적일 것이다.
반면, prvalue는? 얘는 복사도 이동도 참조도 불가능하므로 그냥 값형으로 추론할 수 밖에 없다.
한 번 정리해보면 아래와 같다.
//식의 자료형을 그대로 가져오고 싶을 때
decltype(식) << 형태로 사용하면 된다.
//식의 자료형을 가져오되, 불필요한 복사를 막고싶다면
decltype((식)) << 형태로 사용하면 된다.
decltype((식)) 의 형태에서
(식)이 lvalue 라면, &으로 추론된다.
(식)이 xvalue 라면, &&으로 추론된다.
(식)이 prvalue 라면, 값형으로 추론된다.
그런데, auto가 아니라 굳이 decltype을 사용할 필요가 있을까? 좀 더 복잡해보이는데 말이다.
위에서 말했듯이 auto는 참조형, const, volatile같은 성질은 무시해버린다. 반면, decltype은 이 성질을 그대로 가져온다.
그러므로 자료형을 정확하게 추론하고 싶을 때엔 decltype을 쓰는 것이 좋다.
또한, auto는 초기값이 존재해야 하지만 decltype은 초기값이 없어도 되므로 함수의 파라미터에도 사용이 가능하다.
(C++ 20부터는 auto를 사용하는 것도 가능하지만, 아래 버전에선 auto를 파라미터에 사용할 수 없다.)
auto가 decltype보단 사용이 편하고 코드도 간결해지기 때문에, 상황에 맞게 잘 사용하면 될 것 같다.
'C++ > C++' 카테고리의 다른 글
C++ - Attribute ([[noreturn]], [[nodiscard]] 등) (0) | 2024.11.19 |
---|---|
C++ - weak_ptr 을 안전하게 사용하는 방법 (0) | 2024.11.15 |
C++ - Rule of Zero/Three/ Five (0) | 2024.10.10 |
C++ - 디버깅을 위한 로그 추적 (TRACE, std::source_location) (3) | 2024.10.05 |
C++ - if constexpr (1) | 2024.10.05 |