일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 다형성
- 언리얼
- lua
- 티스토리챌린지
- 메타테이블
- UE4
- 오블완
- effective modern c++
- 참조자
- 상속
- 영화
- operator new
- 예외
- exception
- implicit conversion
- virtual function
- c++
- resource management class
- 영화 리뷰
- 반복자
- Effective c++
- 함수 객체
- 스마트 포인터
- 비교 함수 객체
- Smart Pointer
- reference
- 게임
- 암시적 변환
- more effective c++
- effective stl
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 1. 템플릿 타입 추론 규칙 본문
728x90
항목 1. 템플릿 형식 연역 규칙을 숙지하라
auto의 타입 추론
- 모던 C++의 강력한 기능 중 하나인 auto는 템플릿 타입 추론을 기반으로 작동한다.
- 즉, C++98에서 템플릿 타입 추론에 만족했다면, auto 역시 만족할 것이다.
- 하지만, auto는 템플릿에 비해 덜 직관적일 때가 왕왕 있다.
- 그러므로 우선 템플릿 타입 추론 규직을 잘 짚어볼 필요가 있다.
템플릿의 타입 추론
template<typename T>
void f(ParamType param);
f(expr); // 어떤 표현식으로 f를 호출
- 컴파일러는 expr(표현식)을 이용해서 두 가지 타입을 추론한다.
- T 타입
- ParamType 타입
- 위의 두 타입은 같을 수도 있지만, 보통은 다르다. 아래의 예제로 명확해진다.
template<typename T>
void f(const T& param); // ParamType은 const T&
int x = 0;
f(x); // int로 f 호출. ParamType은 const int&로 추론된다.
- T가 함수에 전달된 인수의 타입과 같을 거라고(T == expr(표현식)의 타입) 생각하겠지만, 꼭 그렇지는 않다.
- T의 타입은 전달된 expr(표현식)의 타입, ParamType의 형태, 두 가지 모두에 영향을 받아 추론된다.
- ParamType이 가질 수 있는 세 가지 형태
- ParamType이 포인터나 참조 타입이지만, 보편 참조(universal reference) (항목 24 참조) 는 아닌 경우
- ParamType이 보편 참조인 경우
- ParamType이 포인터도 참조도 아닌 경우
1. ParamType이 포인터나 참조 타입이지만, 보편 참조는 아닌 경우
- 타입 추론은 아래 순서로 진행된다.
- expr이 참조 타입이라면 참조 부분은 무시한다.
- expr의 타입을 ParamType에 패턴 매칭 시켜 T의 타입을 결정한다.
template<typename T>
void f(T& param); // param은 참조 타입
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T는 int | param의 타입은 int&
f(cx); // T는 const int | param의 타입은 const int&
f(rx); // T는 const int | param의 타입은 const int&
// 1. 전달된 참조 타입은 무시된다.
// 2. 전달된 상수성은 타입의 일부가 된다.(패턴 매칭)
- 만약 T&를 const T&로 바꾼다면?
template<typename T>
void f(const T& param); // param은 const &
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T는 int | param의 타입은 const int&
f(cx); // T는 int | param의 타입은 const int&
f(rx); // T는 int | param의 타입은 const int&
// 1. 전달된 참조 타입은 무시된다.
// 2. 전달받는 쪽에 const가 이미 포함되어 있으므로 T 타입에는 상수성이 포함되지 않는다.
- T가 포인터라면?
template<typename T>
void f(T* param); // param은 포인터
int x = 27;
const int* px = &x;
f(&x); // T는 int | param의 타입은 int*
f(px); // T는 const int | param의 타입은 const int*
// 1. 전달된 포인터 타입은 무시되지 않는다.
// 2. 상수성은 타입의 일부로 포함되어 추론된다.
- 즉, 이 경우 타입은 우리가 예측하는 방식으로 추론된다.
2. ParamType이 보편 참조인 경우
- 매개변수의 선언은 rvalue 참조와 같은 모습이지만(T&&), lvalue가 전달되면 다르게 행동한다.(항목 24 참조)
- 타입 추론은 아래 순서로 진행된다.
- expr이 lvalue라면, T와 ParamType 둘 다 lvalue로 추론된다.
- 이상한 점 1. 템플릿 타입 추론에서 T가 참조 타입으로 추론되는 경우는 이 상황이 유일하다.
- 이상한 점 2. ParamType의 선언 구문은 rvalue 참조와 같은 모습이지만, 추론된 결과물은 lvalue 참조다.
- expr이 rvalue라면 정상적인 규칙(1번 규칙)들이 적용된다.
- expr이 lvalue라면, T와 ParamType 둘 다 lvalue로 추론된다.
template<typename T>
void f(T&& param); // 보편 참조
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x 는 lvalue이므로, T는 int& | param의 타입은 int&
f(cx); // cx는 lvalue이므로, T는 const int& | param의 타입은 const int&
f(rx); // rx는 lvalue이므로, T는 const int& | param의 타입은 const int&
f(27); // 27은 rvalue이므로, T는 int | param의 타입은 int&&
- 즉, 보편 참조가 관여하는 경우 lvalue 인수와 rvalue 인수는 서로 다르게 추론된다.
3. ParamType이 포인터도 참조도 아닌 경우
- 즉, 값으로 전달되는 상황이다.
- 타입 추론은 아래 순서로 진행된다.
- expr의 형식이 참조라면, 참조 부분은 무시된다.
- expr이 const라면 const 역시 무시한다.
- expr이 volatile이라면 그 역시 무시한다.(항목 40 참조)
template<typename T>
void f(T param); // 값으로 전달
int x = 27;
const int cx = x;
cont int& rx = x;
f(x); // T는 int | param의 타입은 int
f(cx); // T는 int | param의 타입은 int
f(rx); // T는 int | param의 타입은 int
- param은 전달되는 값의 복사본이므로 상수성이 떨어져나가는 것은 당연하다.
- 만일 expr이 const 객체를 가리키는 const 포인터이고 param에 값으로 전달되는 경우는 어떻게 될까?
template<typename T>
void f(T param); // 값으로 전달
const char* const ptr = "Fun with pointers"; // const 객체를 가리키는 const 포인터
f(ptr); // 이 경우 T와 param의 형식은?
// 정답은, const char* 이다.
// ptr에 대한 상수성은, 복사로 인해 제거되기 때문이다.
// 하지만 ptr가 가리키는 것의 상수성은 보존된다.
4. 배열 인수
- 위의 상황에 더해 틈새 상황을 추가로 확인해보도록 하자.
- 배열은 포인터 타입과 사실상 다르다.
- 대부분의 문맥에서 배열은 배열의 첫 원소를 가리키는 포인터로 붕괴(decay)된다.
const char name[] = "J. P. Briggs";
const char* ptrToName = name; // 문제 없이 통과
- 그렇다면 템플릿에 전달된다면?
template<typename T>
void f(T param);
f(name); // name은 배열이지만, T는 const char*로 추론된다.
- void f(const char param[]) 으로 추론되지 않는 이유는?
- 사실상 배열 타입의 함수 매개변수라는 것은 없기 때문이다.
- C++에서 위 문법은 합법적이지만, 실질적으로는 아래와 동일하게 취급된다.
- void f(const char* param)
- 하지만, 배열 참조 타입의 함수 매개변수라는 것은 존재한다는 맹점이 있다!
template<typename T>
void f(T& param);
const char name[] = "J. P. Briggs";
f(name); // 배열을 f에 전달하면?
// T는 const char (&)[13]으로 추론된다!!
- 트릭! 이를 활용하면 배열 원소 갯수를 추론하는 템플릿도 만들 수 있다.
// 배열의 크기를 컴파일 시점 상수로 리턴하는 템플릿 함수
// 매개변수의 이름은 어차피 사용하지 않으므로 굳이 붙이지 않음.
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
// 아래와 같이 사용 가능해진다.
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
int mappedVals[arraySize(keyVals)];
- constexpr로 선언하면 함수 호출의 결과를 컴파일 도중에 사용할 수 있게 된다.(항목 15 참조)
- noexcept로 선언한 것은 컴파일러가 더 나은 코드를 산출하도록 하려는 것이다.(항목 14 참조)
5. 함수 인수
- 함수 타입 역시 함수 포인터로 붕괴될 수 있다.
void someFunc(int, double); // someFunc는 함수
template<typename T>
void f1(T param); // param은 값 전달
template<typename T>
void f2(T& param); // param은 참조 전달
f1(someFunc); // param은 함수 포인터로 추론된다.(붕괴 적용)
// 타입은 void (*)(int, double)
f2(someFunc); // param은 함수 참조로 추론된다.
// 타입은 void (&)(int, double)
728x90
'개발 > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 5. 타입 명시보다 auto (0) | 2025.02.11 |
---|---|
[Effective Modern C++] 4. 추론 타입 파악하기 (0) | 2025.02.10 |
[Effective Modern C++] 3. decltype (0) | 2025.02.07 |
[Effective Modern C++] 2. auto 타입 추론 규칙 (0) | 2025.02.06 |
Comments