«   2024/03   »
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 29 30
31
Archives
Today
Total
03-28 16:32
관리 메뉴

lancelot.com

type deduction 본문

프로그래밍

type deduction

lancelot50 2022. 8. 13. 23:43
  • type deduction 이란?
    • 컴파일러가 주어진 조건(표현식)을 가지고 타입을 결정하는 과정
  • type deduction 이 발생하는 경우
    • template
    • auto
    • decltype
    • auto 와 template 은 동일한 규칙을 사용
int main()
{
	int n = 10;
	const int c = 10;

	auto a1 = n;	// int a1=n;
	auto a2 = c;	// (1) const int a2=c;
			// (2) int a2=c;  ==> ok
}

 

  • 추론된 타입을 조사하는 방법
    1. typeid(T).name()
      • g++ : 실행파일 이름 | c++filt -t
      • const / volatile / reference 정보가 출력되지 않는다.
    2. error 메세지 확인
      • 의도적으로 에러를 발생시키면 에러 메세지 안에서 타입 확인 가능
    3. "boost::type_index" 라이브러리 사용
      • type_id_with_cvr<T>().pretty_name()
      • boost를 설치해야 한다.
    4. 컴파일러가 제공하는 "함수이름을 담은 매크로"
      • __FUNCTION__ : C++ 표준. 함수 이름만 나타냄
      • __PRETTY_FUNCTION__ : g++, clang++
      • __FUNCSIG__ : Visual studio ( cl.exe )

 

  • Template type deduction 에서의 핵심 사항
    • T arg : 인자를 값으로 받을 때
    • T& arg : 인자를 lvalue reference 로  받을 때
    • T&& arg : 인자를 rvalue reference 로 받을 때
    • 인자로 배열이 전달될 때
  • 규칙1. template 인자를 값(T arg) 으로 받을 때
    • 새로운 복사본 객체가 만들어져 값을 복사 받는 것
    • 함수 "인자의 const, volatile, reference를 제거하고 T의 타입을 결정" 한다.
    • 주의! 인자의 const 속성만 제거 된다. "인자가 가리키는 곳의 const 속성은 유지"

인자를 값으로 받을 때 - T arg

#include<iostream>

template<typename T>
void foo(T arg)
{
	std::cout << __FUNCSIG__ << std::endl;
}

int main()
{
	int n = 10;
	int& r = n;
	const int c = 10;
	const int& cr = c;

	foo(n);	// T=int
	foo(r);	// T=int
	foo(c); // T=int
	foo(cr);// T=int
}
  • 인자의 const 속성만 제거된다. "인자가 가리키는 곳의 const 속성은 유지"
#include<iostream>

template<typename T>
void foo(T arg)
{
	std::cout << __FUNCSIG__ << std::endl;
}

int main()
{
	const char* const s = "hello";
	foo(s);
}

인자의 const만 제거

 

  • 규칙2. Template 인자를 lvalue reference(T& arg) 로 받을때
    • T의 타입과 arg의 타입은 다르다
    • 함수 인자의 "reference 를 제거하고 T의 타입을 결정"한다.
    • 인자가 가진 "const, volatile 속성은 유지"

T& arg

#include<iostream>

template<typename T>
void foo(T& arg)
{
	std::cout << __FUNCSIG__ << std::endl;
}

int main()
{
	int n = 10;
	int& r = n;
	const int c = 10;
	const int& cr = c;

	foo(n);	// T=int		arg=int&
	foo(c); // T=const int	arg=const int&
	foo(r);	// T=int		arg=int&
	foo(cr);// T=const int	arg=const int&
}

 

  • template 인자에 배열을 전달할 때
    • T arg = x
      • int arg[3] = x;  이 코드는 컴파일 에러
      • int* arg = x;   이 코드는 컴파일 에러 아님
    • T& arg = x
      • int (&arg)[3]=x; // 배열을 가리키는 참조 컴파일 에러 아님
#include<iostream>

template<typename T>
void foo(T arg)
{
	std::cout << __FUNCSIG__ << std::endl;
}

template<typename T>
void goo(T& arg)
{
	std::cout << __FUNCSIG__ << std::endl;
}

int main()
{
	int x[3] = { 1,2,3 };
	foo(x);	// T=int* arg=int*
	goo(x);	// T=int[3] arg=int(&)[3]
}
  • T arg : T는 포인터( int *)
  • T& arg
    • T는 배열( int[3])
    • arg는 배열을 가리키는 참조( int (&)[3] )

 

  • 또다른 예시
#include<iostream>

template<typename T>
void foo(T arg1, T arg2)
{
}

template<typename T>
void goo(T& arg1, T& arg2)
{
}

int main()
{
	foo("orange", "apple");	// ok
	// foo(const char[7], const char[6])
	// 하지만 T 일때는 *로 받음

	goo("orange", "apple");	// error
	// goo( const char[7], const char[6])
	// 배열 갯수가 다르면 타입이 다름

	goo("orange", "banana");	// ok

}

 

  • auto type deduction
    • template : 함수 인자로 타입 추론 - T arg = 함수인자;
    • auto : 우변의 표현식으로 타입 추론 - auto a = 표현식;
  • auto 와 배열, int x[3] 일 때,
    • auto a=x : auto 는 포인터 ( int* )
    • auto& a=x : auto 는 배열 ( int[3] ), a는 배열을 가리키는 포인터( int(&)[3] )
int main()
{
	int n = 10;
	int& r = n;
	const int c = 10;
	const int& cr = c;

	// 규칙 1
	// T arg : 함수 인자의 const, volatile, reference 를 제거하고 T의 타입 결정
	auto a1 = n;	// auto int
	auto a2 = r;	// auto int
	auto a3 = c;	// auto int
	auto a4 = cr;	// auto int

	// 규칙2
	// T& arg : 인자의 reference 만 제거하고 T의 타입 결정. const, volatile 은 유지
	auto& a5 = n;	// auto int			a5=int&
	auto& a6 = r;	// auto int			a6=int&
	auto& a7 = c;	// auto const int	a7=const int &
	auto& a8 = cr;	// auto const int	a8o=const int &

	int x[3] = { 1,2,3 };
	auto a = x;		// auto int*
	auto& b = x;	// auto int[3]	b=int(&)[3]
}

 

  • int vs std::initializer_list
#include<iostream>
#include<vector>

int main()
{
	auto a1 = 1;	// int
	auto a2 = { 1 };// std::initializer_list<int>
	auto a3{ 1 };	// int

	std::cout << typeid(a1).name() << std::endl;
	std::cout << typeid(a2).name() << std::endl;
	std::cout << typeid(a3).name() << std::endl;

	std::vector<int>	v1(10, 0);
	std::vector<bool>	v2(10, false);

	auto a4 = v1[0];	// auto=int
	auto a5 = v2[0];	// auto= "proxy 객체"

	// std::vector<bool>
	// -> 최적화를 위해 specialization 되어있음
	// -> [] 연산자가 bool 로 변환가능한 "Temporary proxy 를 반환"

	std::cout << typeid(a4).name() << std::endl;
	std::cout << typeid(a5).name() << std::endl;
}
  • std::vector<bool>
    • 최적화를 위해 specialization 되어있음
    • [] 연산자가 bool 로 변환가능한 "Temporary proxy 를 반환"
    • 구현에 따라 다를 수 있음

 

  • decltype type deduction
    • decltype(expression)
      • ( ) 안에 있는 표현식으로 타입 결정
    • 2가지 규칙
      • ( ) 안에 있는 표현식이 심볼의 이름만 있는 경우
      • ( ) 안에 심볼 외에 연산자 등을 포함함 표현식이 있는 경우
    • 규칙1. ( ) 안에 심볼의 이름만 있는 경우
      • 심볼의 선언을 통해서 심볼과 동일한 타입으로 결정
#include<iostream>

int main()
{
	int n = 10;
	int& r = n;
	int* p = &n;
	const int c = 10;
	const int& cr = c;

	decltype(n) d1;	// int d1
	decltype(r) d2;	// int& d2		 -> 초기화 문제로 error
	decltype(p) d3;	// int* d3 
	decltype(c) d4;	// const int d4  -> 초기화 문제로 error
	decltype(cr) d5;// const int& d5 -> 초기화 문제로 error
}

 

  • (참고) 함수의 반환 타입
    • 값 타입으로 반환하는 함수
      • 등호의 왼쪽에 올 수 없다. 
      • lvalue가 될 수 없다.
    • 참조 타입으로 반환하는 함수
      • 등호의 왼쪽에 올 수 있다.
      • lvalue가 될 수 있다.
  • 규칙 2. ( ) 안에 심볼 이름 외에 연사잔 등을 포함한 표현식이 있는 경우
    • 표현식의 결과가 등호의 왼쪽에 올 수 있으면 reference 타입( &, lvalue reference )
    • 표현식의 결과가 등호의 왼쪽에 올 수 없으면 값 타입
    • 표현식이 move() 와 같이 rvalue reference 를 반환하면 rvalue reference 타입(&&)
int x = 10;
int foo() { return x; }
int& goo() { return x; }

int main()
{
//	foo() = 20;	// 10 = 20	error
	goo() = 20;	// x의 별명 = 20	ok

	int n = 10;
	int* p = &n;

	decltype(p)		d1;	// int* d1;
//	decltype(*p)	d2;	// int& d2;	 -> *p=30   가능 초기값 문제로 error

	decltype(n)		d3;	// int	d3;  
	decltype(n+1)	d4;	// int	d4;	 -> n+1=30	불가능
//	decltype((n))	d5;	// int& d5;	 -> (n)=30  가능  초기값 문제로 error 
//	decltype(n=20)	d6;	// int& d6;	 -> n=20=30 가능  초기값 문제로 error
	
	int x[3] = { 1,2,3 };

//	decltype(x[0]) d7;	// int& d7;		 초기값 문제로 error
	auto a1 = x[0];	// int a1  -> x[0] 가 int& 인데, auto 에서는 &가 제거되므로 auto가 int가 되는 것.

	decltype(x) d8;		// int d8[3];
	auto a2 = x;		// int a2[3]=x;  에러이므로
						// int* a2=x;

	decltype(n++)	d9;	// int d9;		n이 표현식에 참여하고 ++는 나중에 적용
//	decltype(++n)	d10;// int& d10	++n 이 표현식이므로 규칙2가 되어서 int& -> &의 초기화가 없어서 error
}
decltype( *p ) *p = 30 ok int&
decltype( n+1 ) n+1 = 30 error int
decltype( (n) ) (n) = 30 ok int&
decltype( n=20 ) n=20 = 30 ok int&
decltype( x[0] ) x[0] = 30 ok int&
decltype( x ) 규칙 1   int[3]

 

  • decltype( auto )
    • decltype( 함수이름 )
      • 함수의 타입
    • decltype( 함수호출식
      • 함수 실행의 결과 타입, 즉, 함수 반환타입
      • 실제 함수가 호출되는 것은 아님.
      • 함수 구현이 업어도 선언만 있으면 사용가능
      • "평가되지 않은 표현식( unevaluated expression )"
    • decltype( auto )
      • auto 위치에 우변 표현식을 넣어서 타입 추론
      • 즉, 우변 표현식으로 타입을 추론하는데, 규칙은 decltype 규칙 사용
      • C++14 부터 사용가능
#include<iostream>

int x = 10;
int foo(int a, int b) { return x; }
int& goo(int a, int b) { return x; }

int main()
{
	decltype(foo)		d1;	// int(int, int)
					// int d1(int, int)
	decltype(foo(1,2))	d2;	// int d2;
//	decltype(goo(1, 2))	d3;	// int& d3;

	auto ret1 = goo(1, 2);	// int ret1=goo(1,2);

	decltype(goo(1, 2)) ret2 = goo(1, 2);
	// int& ret2=goo(1,2);
	// 위의 표현이 복잡하니, 아래처럼 auto를 쓸수있다.
	decltype(auto) ret3 = goo(1, 2);

	ret3 = 1000;
	std::cout << x << std::endl;

}

 

  • decltype( auto ) 
    • decltype( a+b ) Add(T1 a, T2 b)
      • 컴파일 에러.  선언되지 않은 변수 사용
    • auto Add(T1 a, T2 b) -> decltype( a+b )
      • 에러 아님. 정확한 타입으로 반환
    • auto Add(T1 a, T2 b)
      • 에러 아님
      • 리턴문의 표현식이 참조라면, 참조를 제거한 타입 반환(auto의 규칙을 따르므로)
    • decltype(auto) Add(T1 a, T2 b)
      • 정확한 타입으로 반환
      • 가장 널리 사용
      • C++14부터 가능, C++11이라면 2번째 표기법 사용
#include<iostream>

template<typename T> T Add(T a, T b)
{
	return a + b;
}

// template<typename T1, typename T2> decltype(a+b) Add(T1 a, T2 b)  error 선언되지 않은 변수 사용
// template<typename T1, typename T2> auto Add(T1 a, T2 b) -> decltype(a+b)  // ok 정확한 타입으로 반환
// template<typename T1, typename T2> auto Add(T1 a, T2 b)  // ok. 하지만 auto의 규칙을 따르기때문에, return문의 표현식이 참조라면 참조를 제거한 타입을 반환
template<typename T1, typename T2> decltype(auto) Add(T1 a, T2 b)  // ok. 정확한 타입으로 반환. 가장 널리사용. C++14부터 가능. C++11이라면 2번째 표기법 사용
{
	return a + b;
}

int main()
{
	std::cout << Add(1,2) << std::endl;
	std::cout << Add(1, 2.3) << std::endl;
}