* pow

 

template<int m, int n>
struct MyPow
{
    static const long long result = m * MyPow<m, n - 1>::result;
};

template<int m>
struct MyPow<m, 0>
{
    static const long long result = 1;
};

 

 

* factorial

 

template<int n>
struct MyFactorial
{
	static const long long result = n * MyFactorial<n - 1>::result;
};

template <>
struct MyFactorial<1>
{
	static const long long result = 1;
};

 

 

* enable_if

 

template <bool condition, typename T = void>
struct MyEnableIf {}; // condition이 false일때는 정의되지 않는다.

template <typename T> // condition이 true일때만 타입을 정의한다.
struct MyEnableIf<true, T> { using type = T; }; 

template <typename T>
typename MyEnableIf<std::is_integral<std::remove_reference_t<T>>::value, bool>::type
                Multiply(T&& a, T&& b) { return a * b; }

 

 

 

* is_prime

 

// 나눠 떨어지면 재귀를 빠져나가도록 설계한다.
template <int n, int d>
struct Divisible
{
    static const bool result = (0 == n % d) ? true : Divisible< n, d - 1 >::result;
};

// 2에서 브래이크를 걸어준다.
template <int n>
struct Divisible <n, 2>
{
    static const bool result = (0 == n % 2);
}; 


template <int n>
struct MyIsPrime 
{
    static const bool result = (false == Divisible< n, n / 2 >::result);
};

template <>
struct MyIsPrime<2> // 2는 소수, n / 2 부터 나눠서 확인하기 때문에 예외처리
{
    static const bool result = true;
};

template <>
struct MyIsPrime<3> // 3은 소수, n / 2 부터 나눠서 확인하기 때문에 예외처리
{
    static const bool result = true;
};

 

'C++' 카테고리의 다른 글

템플릿 메타 프로그래밍(Template Meta Programming, TMP)  (0) 2022.05.23
가변인자 템플릿(Variadic Template) 예시  (0) 2022.05.23
가변인자 템플릿(Variadic Template)  (0) 2022.05.23
템플릿(Template)  (0) 2022.05.23
람다(Lambda)  (0) 2022.04.11

* 정의

 

템플릿 메타 프로그래밍(template meta programming, TMP)은 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 것을 말한다. 템플릿 기반 프로그램을 작성함으로써 컴파일러에게 프로그램 코드를 생성하도록 하여 컴파일 시점에 많은 것들을 결정하도록 만든다.

 

일반적인 프로그래밍 방식에서 실행 시점에서 대부분 수행한다면 템플릿 메타 프로그래밍 방식에서는 컴파일 시점에 많은 것들을 수행하여 실행 시점에서의 연산을 줄인다.

 

템플릿 메타 프로그램은 변수의 값이 한번 정해지면 변경할 수 없기 때문에 함수형 프로그래밍의 한 형태로 볼 수 있다. 많은 템플릿 구현에서 흐름 제어가 재귀를 통해 수행된다. 유사한 코드 중복을 피하는 일반화 프로그래밍과도 연관있다.

 

 

* 장점

 

템플릿 메타 프로그래밍의 장점, 템플릿의 장점은 다음과 같이 정리할 수 있다.

 

- 컴파일 시점에 연산을 하기 때문에 실행 시점에서 연산을 하거나 함수를 별도로 호출하는 시간을 아낄 수 있다. 또한 컴파일 시간에 확인하기 때문에 문제점을 더 빠르게 파악할 수 있다.

 

- 상수전파(constant propagation)이 더 잘 일어날 수 있다.

여기서 상수전파란 컴파일러의 실행시간 최적화 기법으로 컴파일 시 상수를 포함하는 연산이 계산될 수 있으면 계산하두어 코드를 줄이는 방식이다. 다음과 같은 코드가 있다고 가정할 때 y의 값은 컴파일 시 6으로 계산된다.

x = 3;
y = 2 * x;

 

 

- 정적 다형성을 구현할 수 있다.

동적 다형성(가상 함수)은 알맞은 가상함수를 호출하기 위해 vtable을 확인해서 적절한 함수를 호출하게 되며 이는 오버헤드로 이어진다. 정적 다형성을 구현하면 더 성능이 좋다. 일반적인 경우에는 부모 클래스에서 자식 클래스의 정보를 모르지만 템플릿을 활용하면 가능하다. 다음과 같이 구현된다.

template <typename Super>
class Base
{
public:
    void doSomething(void)
    {
        // ...
        static_cast<Super*>(this)->doSomethingImpl();
        // ...
    }
};

class Derived : public Base<Derived>
{
public:
    void doSomethingImpl(void)
    {
        // ...
    }
};

 

 

- 상수만 사용될 수 있는 문맥에 템플릿 정보를 활용할 수 있다. 예를 들어 배열의 원소 개수를 지정할 때는 무조건 상수의 값을 받을 수 있는데, 이에 템플릿을 사용할 수 있다. 이를 활용하면 힙에 할당하지 않고 스택에서 처리할 수 있다.

// std::array
template <class _Ty, size_t _Size>
class array { // fixed size array of values
public:
// ...
    _Ty _Elems[_Size];
};

 

- 내부 자료형이 확정되지 않아도 클래스나 함수를 만들어내는 것이 가능하다. 일반화 프로그래밍(generic programming)에 특화되어 있다고 보면 된다.

 

- 가변인자를 전달할 수 있으며 일반 가변 함수 매개변수나 매크로와 달리 타입 안정성을 갖출 수 있다.

https://create-new-worlds.tistory.com/303

 

가변인자 템플릿(Variadic Template) 예시

* printf 일반 가변인자 함수와 비교했을 때 가변인자 템플릿은 타입 안정성을 갖출 수 있다. 일반적으로 우리가 사용하는 printf의 경우 %d와 같이 어떤 형식으로 출력해야할 지 정해줘야 한다. 하

create-new-worlds.tistory.com

 

- 암시적 자료 변환을 막기 위해 컨디션에 따라 특정 타입을 선언하는 std::enable_if와 같은 함수를 활용할 수 있다.

std::enable_if는 조건에 따라 해당 타입을 정의할지 정의하지 않을 지 결정하는 기법으로 조건에 맞지 않는다면 컴파일 에러를 발생시키게 된다. 사용 예시는 다음과 같다.

// 정수형 타입이 들어왔을 때만 반환형인 bool이 정상적으로 정의된다.
template <typename T>
typename std::enable_if<std::is_integral<std::remove_reference_t<T>>::value, bool>::type 
is_odd(T&& i) { return bool(i % 2); }


// 두번째 템플릿 인자는 조건 체크를 위해 넣어둔 것이다. 이를 숨기기 위한 두가지 방법이 존재한다.
// 1. 뒤에 = 0을 붙인다. 2. 앞에 typename = 을 붙인다.

// typename = 방식
template<typename T, typename = typename std::enable_if<std::is_integral<std::remove_reference_t<T>>::value, void>::type>
bool is_even(T&& i) { return !bool(i % 2); }

// = 0 방식
template<typename T, typename std::enable_if<std::is_integral<std::remove_reference_t<T>>::value, int>::type = 0>
bool is_even1(T&& i) { return !bool(i % 2); }

 

 

* 단점

 

- 컴파일 시점에 연산해야하는 것들이 많아서 컴파일 시간이 늘어난다.

 

- 특정 소스파일 내부에서만 사용하는 것이 아니라면 항상 헤더 파일에 있어야 한다. 

 

- 통상적인 반복문 대신 재귀 호출로 구현해야 한다.

 

- 일반 코드에 비해 가독성이 떨어진다.

 

- 디버깅하기 매우 어렵다. 오류 메시지를 보고 판단해야하는데 오류 메시지도 템플릿과 연관되어 있기 때문에 너무 많고 복잡하다.

 

- 배포 시 다른 개발자들에게 내부 코드를 강제로 공개하게 된다.

 

- 컴파일러마다 템플릿 사용 방법이 조금씩 다를 수 있어서 이식성에 문제가 있을 수 있다.

 

 

* 예시

 

https://create-new-worlds.tistory.com/307

 

템플릿 메타 프로그래밍(Template Meta Programming, TMP) 예시

* pow template struct MyPow { static const long long result = m * MyPow ::result; }; template struct MyPow { static const long long result = 1; }; * factorial template struct MyFactorial { static c..

create-new-worlds.tistory.com

 

 

 

* printf

 

일반 가변인자 함수와 비교했을 때 가변인자 템플릿은 타입 안정성을 갖출 수 있다. 일반적으로 우리가 사용하는 printf의 경우 %d와 같이 어떤 형식으로 출력해야할 지 정해줘야 한다. 하지만 가변인자 템플릿은 해당 인자의 자료형을 알 수 있기 때문에 %의 정보만 있으면 된다.

void MyPrintf(const char* inputString)
{
    std::cout << inputString;
}

template<typename T, typename... Ts>
void MyPrintf(const char* inputString, T&& arg, Ts&&... args)
{
    while ('\0' != *inputString)
    {
        if ('%' == *inputString)
        {
            ++inputString;
            std::cout << arg;

            MyPrintf(inputString, std::forward<Ts>(args)...);
            return;
        }

        std::cout << *(inputString++);
    }
}


int main(void)
{
    MyPrintf("string : %, float : %, char : %, int : %",
             "testString",
             10.022f,
             'b',
             39292);
             
    return 0;
}
string : testString, float : 10.022, char : b, int : 39292

 

 

* ...

 

* 정의

 

가변인자 템플릿(variadic template)은 0개 이상의(고정 개수가 아닌) 변수를 인수로 사용할 수 있는 클래스나 함수 템플릿의 한 종류이다. 간단히 여러 개의 인수를 사용할 수 있는 템플릿이다.

 

가변인자 템플릿과 반대로 일반 템플릿은 선언 시 지정된 고정 개수의 매개변수만 가질 수 있다.

 

 

* 동작 방식

 

- 기본 형태

 

일반적으로 다음과 같이 사용된다.

필자는 직관적으로 이해하기 위해 (타입들... 여러 타입) 을 가진 (여러타입(타입들 ...) ...여러변수)와 같이 이해하였다.

template <typename T, typename... Types>
ReturnType FunctionName(T current, Types... rest)
{
    FunctionName(rest...);
    // ...
}

...으로 표현되는 부분의 의미

  • ...가 왼쪽에 붙는 경우 : 파라미터 팩(parameter pack), 0개 이상의 함수 인자를 나타낸다.
  • ...가 오른쪽에 붙는 경우 : 파라미터 팩을 풀어서 인자로 분리하는 역할을 한다.

 

가변인자들은 인자들을 순회할 수 있고, 인자들의 개수를 알 수 있고, 인덱스를 통해 값에 접근할 수 있으며 템플릿의 인자를 분할할 수 있다.

 

 

- 넘겨줄 수 있는 가변인자들의 개수

 

템플릿에 0개 이상의 가변인자를 넘겨줄 수 있다. 0개도 가능하다. 즉 다음과 같은 형식도 허용된다.

template<typename... Types>
class Tuple { /* ... */ };

Tuple<> emptyTuple;

 

1개이상으로 보장하고 싶다면 다음과 같이 가변인자 템플릿을 정의하면 된다.

template<typename T, typename... Types>
class Tuple_1 { /* ... */ };

// 컴파일 에러가 발생한다.
//Tuple_1<> tuple_1_0;

Tuple_1<int> tuple_1_1;
Tuple_1<int, float, long long> tuple_1_3;

 

 

- 가변인자들의 개수

 

가변인자들의 개수는 sizeof...()함수를 통해서 알 수 있다.

 

다음 예시는 가변인자를 받아서 float형으로 평균을 반환하는 함수이다. sizeof...가 활용된다.

float Sum_f(void)
{
    return 0.0f;
}

template<typename T, typename... Ts>
float Sum_f(T&& arg, Ts&&... args)
{
    return arg + Sum_f(std::forward<Ts>(args)...);
}

template<typename... Ts>
float Average_f(Ts&&... args)
{
    return Sum_f(std::forward<Ts>(args)...) / sizeof...(Ts);
}

 

 

- 가변인자들의 활용

 

가변인자 자체는 함수나 클래스의 구현에 쉽게 사용할 수 없다. 그래서 템플릿을 재귀호출을 통해 단계적으로 분할하면서 활용하는 경우가 있다. 이 때문에 가변인자 템플릿은 재귀로 구현되어 있는 경우를 볼 수 있다. 이를 C++17부터는 fold expression이라는 형식으로 매우 간단하게 표현할 수 있도록 지원하고 있다.

 

 

@ 재귀 호출

바로 위에 나온 코드도 재귀 호출을 통해 구현되었다.

간단히 받은 인자들을 콘솔창에 출력하는 가변인자 템플릿의 예시

void print(void)
{
    cout << " :: print empty\n";
}

template <typename T, typename... Ts>
void print(T&& arg, Ts&&... args)
{
    cout << arg << " / ";
    print(std::forward<Ts>(args)...);
}

int main(void)
{
    print(36.2f, 3000'000'000, 'a', 21000,"test1","test2");
	return 0;
}
36.2 / 3000000000 / a / 21000 / test1 / test2 /  :: print empty

간단히 설명을 하면 print에 여러 인수를 넘겨주게 되면 컴파일러는 해당 인수를 받을 수 있는 함수를 찾아본다. 결국 가변인자 템플릿의 print함수가 선택되고 다시 비슷한 방식으로 재귀적으로 가변인자 템플릿의 함수가 호출된다. 최종적으로 인자가 없는 버전인 print()가 호출되면서 재귀를 탈출하게 된다.

 

 

@ fold expression

fold expression가 어떻게 평가되는지 표로 나타내면 다음과 같다.

fold 형식 평가
Unary right fold (E op ...) (E1 op (... op (EN-1 op EN)))
Unary left fold (... op E)  (((E1 op E2) op ...) op EN)
Binary right fold (E op ... op I) (E1 op (... op (EN−1 op (EN op I))))
Binary left fold (I op ... op E) ((((I op E1) op E2) op ...) op EN)

 

단항 왼쪽 예시

template<typename... Args>
bool all(Args... args) { return (... && args); }
 
bool b = all(true, true, true, false);

//==============================================

template<typename ...Args>
void printer(Args&&... args)
{
    (std::cout << ... << args) << '\n';
}

printer(1, 2, 3, "abc");

 

 

- 가변인자 템플릿 예시

 

https://create-new-worlds.tistory.com/303

 

가변인자 템플릿(Variadic Template) 예시

* printf 일반 가변인자 함수와 비교했을 때 가변인자 템플릿은 타입 안정성을 갖출 수 있다. 일반적으로 우리가 사용하는 printf의 경우 %d와 같이 어떤 형식으로 출력해야할 지 정해줘야 한다. 하

create-new-worlds.tistory.com

 

* 정의

 

템플릿(template)는 주형, 본뜨는 공구라는 뜻을 가지고 있다.

C++에서는 이와 비슷하게 원하는 타입에 따라서 그 타입에 맞게 코드를 생성시켜주는 도구를 의미한다.

데이터 타입을 매개변수로 전달하면 서로 다른 데이터 유형에 대해 동일한 코드를 작성할 필요가 없어진다.

 

 

* 사용 이유

 

함수나 클래스를 처음부터 다시 작성하지 않고 다른 데이터 유형에 따라 생성되도록 해주기 때문에 프로그래밍이 편리하다. 예를 들어 특정 타입의 데이터를 정렬하는 sort라는 함수가 있다고 가정했을 때 각 타입에 대해 여러 함수를 오버로딩하는 것보다 매개변수를 템플릿 타입으로 결정하게 되면 훨씬 관리하기 편하고 오류를 줄일 수 있다.

 

방금 제시한 예시에서와 같이 템플릿은 주로 일반화 프로그래밍(generic programming)에 사용된다. 간단히 일반화된 클래스나 함수를 작성하기 위한 방법으로 사용된다고 할 수 있다.

 

일반화 프로그래밍에 대한 정의는 다음과 같이 할 수 있다.

  • 일반화 유형이 다양한 데이터 타입에 대해 작동하는 알고리즘의 매개 변수로 사용되는 프로그래밍 접근 방식이다.
  • 데이터 형식에 의존하지 않고 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식이다.

 

하지만 템플릿의 단점도 존재하는데 단점을 고려해서 상황에 적절한 판단 하에 템플릿을 사용해야 한다. 템플릿의 단점은 다음과 같다.

  • 많은 컴파일러는 템플릿 중첩을 지원하지 않는다.
  • 템플릿을 사용하면 모든 코드가 추상화되지 않고 노출된다.
  • 일부 컴파일러는 템플릿에 대한 지원을 많이 하지 않는다.
  • 템플릿 코드에서 오류가 감지되면 판단하기 어려운 오류 메시지가 표시된다.
  • 템플릿으로 개발하는 것이 어려울 수 있다.

 

* 동작 방식

 

- 기본 동작 방식

 

템플릿은 컴파일 타임에 확장(결정)된다는 점에서 템플릿과 공통점을 지닌다. 하지만 매크로와는 다르게 컴파일러가 템플릿을 확장(결정)하기 컴파일러가 타입에 대한 체크를 한다는 점이다. 

 

처음 소스 코드에는 함수/클래스 템플릿만 포함하고 있지만 컴파일된 코드에서는 여러 가지 같은 함수/클래스들로 확장된다.

 

- 종류

 

템플릿은 다음과 같이 크게 두 타입으로 나타난다.

  • 함수 템플릿
  • 클래스 템플릿

 

 

 

- 함수 템플릿

 

함수 템플릿은 단일 함수에 대한 템플릿을 의미하며 이는 여러 데이터 타입과 함께 동작한다. 예를 들어 sort, max, min과 같은 함수 템플릿이 여기에 속한다.

template <typename T> T FunctionName(T a, T b)
{
    // ...
}

보통 다음과 같이 정의된다. T를 함수의 반환형이나 매개변수, 함수바디 등에서 활용할 수 있다. type에는 함수에서 사용 될 데이터 타입이 들어간다.

 

 

- 클래스 템플릿

 

클래스 템플릿도 함수 템플릿과 비슷하게 동작한다. 클래스 템플릿은 클래스가 데이터 타입에 관계 없는 것들을 정의할 때 유용하다.

 

예를 들어 컨테이너 클래스의 경우 내부 동작방식은 동일하지만 안에 들어가는 개체의 타입만 다르기 때문에 클래스 템플릿을 활용하면 매우 효율적이다.

template <typename T> 
class Array 
{
public:
	Array(T arr[], int s)
	{
		ptr = new T[s];
		size = s;
		for (int i = 0; i < size; i++)
		{
			ptr[i] = arr[i];
		}
	}

	void print()
	{
		for (int i = 0; i < size; i++)
		{
			cout << " " << *(ptr + i);
		}
		cout << endl;
	}

private:
	T* ptr;
	int size;
};

 

 

- 템플릿 특수화(Template Specialization)

 

템플릿 특수화는 특수한 경우에 대해서 코드를 다르게 작성하여 다른 동작을 수행할 수 있게 만들어주는 기능이다. 특정 유형에 대해서 다른 동작 방식을 정의함으로써 템플릿을 통해 수행될 수 있게 지원할 수 있다. 특수화 버전은 해당 타입의 함수를 호출했을 때 일반적인 템플릿 정의보다 우선 시되어 먼저 체크하게 된다.

//A generic sort function
template<typename T>
{
    //code to implement quick sort
}

//Template specilization:A function
//specialized for char data type
template<>
void sort<char>(char arr[],int size)
{
    //code to impletement counting sort
}

 

 

* 함수 오버로딩 vs 함수 템플릿

 

함수 오버로딩 함수 템플릿
@ 객체 지향 프로그래밍에서 다형성 특성에 속한다.
@ 여러 함수가 비슷한 동작을 수행할 때 사용된다.

@ 함수 오버로딩은 다양한 수의 인수를 취할 수 있다.
@ 여러 함수가 동일한 작업을 수행하지만 데이터 타입만 다르게 정의해야할 때 사용된다.

@ 템플릿은 다양한 인수를 취하지 못한다.

 

'C++' 카테고리의 다른 글

가변인자 템플릿(Variadic Template) 예시  (0) 2022.05.23
가변인자 템플릿(Variadic Template)  (0) 2022.05.23
람다(Lambda)  (0) 2022.04.11
완벽 전달(Perfect forwarding)의 실패  (0) 2022.03.31
참조 축약  (0) 2022.03.31

* 정의

 

완벽 전달(Perfect forwarding)이란 한 함수가 처음 받았던 것과 동일한 인수들 그대로 다른 함수에 넘겨주는 것을 의미한다.

 

값 전달 방식(call by value)에서의 매개변수는 원래의 호출자가 넘겨준 인수의 복사본이기 때문에 불가능하고 포인터 매개변수는 호출자에게 포인터를 넘겨주도록 강제하는 것은 바람직하지 않기 때문에 불가능하다.

 

완벽 전달은 단순히 객체들을 전달하는 것 뿐만아니라 객체들의 주요 특징, 형식, 왼값 오른값, const, volatile 등도 전달하는 것을 의미한다. 이러한 것들을 수행할 수 있는 것은 보편 참조 매개변수뿐이다.

 

 

* 완벽 전달 실패

 

f에 완벽 전달을 수행하는 함수 fwd와 함수 f가 있다고 가정할 때 어떤 인수로 f를 호출했을 때와 fwd를 호출했을 때 결과가 다르다면 완벽 전달이 실패했다고 할 수 있다.

 

 

- 중괄호 초기치

 

중괄호 초기치 사용에서 완벽 전달이 실패한다.

void f(const std::vector<int>& input) {}

template<typename... Ts>
void fwd(Ts&&... params)
{
	f(std::forward<Ts>(params)...);
}

void Test(void)
{
	f({ 1,2,3 });
	
	// 컴파일 에러
	//fwd({ 1,2,3 });
}

f를 직접호출하면 컴파일러는 호출 지점에 함수에 전달된 인수들의 형식들과 f에 선언된 매개변수들의 형식들을 비교해서 호환 여부를 파악하고 필요에 따라 적절한 암묵적 변환을 수행해서 호출을 성사시킨다. 위의 코드에서는 {1,2,3}으로부터 임시 std::vector<int> 객체를 생성해서 f의 매개변수 input에 묶는다.

 

fwd를 통해 간접호출하게 되면 호출 지점에서 전달된 인수들과 f에 선언된 매개변수를 직접 비교할 수 없다. 컴파일러는 fwd에 전달되는 인수들의 형식을 연역하고 연역된 형식들과 f의 매개변수 선언들과 비교한다. 과정에서 다음 조건 중 하나라도 만족되면 완벽 전달이 실패한다.

 

  1. fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 연역하지 못한다.
  2. fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 잘못 연역한다. 잘못 연역하였을 때 그 형식으로 fwd의 인스턴스를 컴파일하지 못하거나 컴파일은 되지만 직접 호출할 때와 다르게 행동하는 결과를 가져온다.

 

여기서 문제는 std::initializer_list가 될 수 없는 형태로 선언된 함수 템플릿 매개변수에 중괄호 초기치를 넘겨준다는 것이다. 간단히 말하면 {1,2,3}의 형식을 컴파일러가 연역하는 것이 금지되어 fwd의 매개변수의 형식을 연역할 수 없는 뜻이다.

 

이를 방지하기 위해 auto 변수를 중괄호 초기치로 초기화하고 넘겨주면 된다.

auto input = { 1,2,3 };
fwd(input);

 

 

- 널 포인터를 의미하는 0 또는 NULL

 

0이나 NULL을 널 포인터로서 템플릿에 넘겨주려 하면 컴파일러가 이를 포인터 형식이 아닌 정수 형식으로 연역하기 때문에 문제가 생긴다. 해결책은 0또는 NULL 대신 nullptr을 사용하면 된다.

 

 

- 선언만 된 정수 static const 및 constexpr 자료 멤버

 

어떤 컴파일러에서는 static const, static constexpr 자료 멤버가 선언만 되어있고 정의가 되어있지 않을 때 해당 변수의 주소값을 취한다면 해당 변수에 대한 저장소가 따로 마련되어있지 않아 링크가 실패하는 경우가 있다.(정의가 없어서)

 

보편 참조 역시 참조를 사용하기 때문에 내부적으로 포인터와 비슷하게 동작하고 있다. 따라서 보편 참조 템플릿 함수 호출에 실패하는 경우가 있다. 해결 방법은 정의를 해주면 된다.

 

 

- 중복적재된 함수 이름과 템플릿 이름

 

함수 포인터를 매개 변수로 받는 함수로 중복적재된 함수를 완벽 전달하려고 할 때 문제가 생긴다.

int InputFunction(int a) { return a; }
int InputFunction(int a, int b) { return a; }

void f(int (*fp)(int)) {}

template<typename... Ts>
void fwd(Ts&&... params)
{
	f(std::forward<Ts>(params)...);
}

void Test(void)
{
	f(InputFunction);

	// 형식이 없다 > 형식연역 불가능
	//fwd(InputFunction);
}

그냥 직접 호출하는 것은 f의 선언에서 매개변수 형식과 일치하는 것을 찾으면 되기 때문에 적절한 중복적재 버전을 선택하여 넘겨줄 수 있다. 하지만 fwd 함수에 넘겨줄 때는 호출에 필요한 형식에 관한 정보가 없어서 어떤 중복적재 버전을 선택할지 결정하지 못한다.

 

이를 해결하기 위해서 전달하고자 하는 중복적재나 템플릿 인스턴스를 명시적으로 지정하면 된다. 다음과 같다.

using InputFunctionType = int (*) (int);
fwd(static_cast<InputFunctionType>(InputFunction));

 

 

- 비트필드

 

특정 빌트필드 직접적으로 지칭하는 방법은 없으며 임의의 비트들을 가리키는 포인터를 생성하는 방법은 없다. 따라서 비트필드를 인수로 받는 임의의 함수는 그 비트필드 값의 복사본을 받게 해주면 된다.

struct IPv4Header
{
	std::uint32_t	version : 4,
					IHL : 4,
					DSCP : 6,
					ECN : 2,
					totalLength : 16;
};

void f(std::uint16_t totalLength) {}

template<typename... Ts>
void fwd(Ts&&... params)
{
	f(std::forward<Ts>(params)...);
}

void Test(void)
{
	IPv4Header header;

	// ...
	
	// 불가능하다.
	//fwd(header.totalLength);

	auto length = static_cast<std::uint16_t>(header.totalLength);
	fwd(length);
}

 

 

'C++' 카테고리의 다른 글

템플릿(Template)  (0) 2022.05.23
람다(Lambda)  (0) 2022.04.11
참조 축약  (0) 2022.03.31
보편 참조에 대한 중복적재 대안  (0) 2022.03.31
보편 참조와 오른값 참조  (0) 2022.03.27

* 정의

 

보편 참조를 받는 함수 템플릿에서 T에 따라 매개 변수는 Type& && (T가 Type&일 때)와 같은 형태가 나타날 수 있다. 이때 Type& && 는 Type&으로 간주된다.

 

원래 참조에 대한 참조가 위법이지만 템플릿 인스턴스화와 같은 특정 문맥에서는 참조에 대한 참조가 허용된다. 또한 이러한 경우 참조 축약의 규칙이 적용된다. (Type& && => Type& 등)

 

 

* 동작 방식

 

- 참조 축약 규칙

 

특정 문맥(템플릿 인스턴스화 등)에서 발생하는 참조 축약의 규칙은 다음과 같다.

참조 유형 설명 평가
Type& & 왼값 참조에 대한 왼값 참조 Type&
Type& && 왼값 참조에 대한 오른값 참조 Type&
Type&& & 오른값 참조에 대한 왼값 참조 Type&
Type&& && 오른값 참조에 대한 오른값 참조 Type&&

 

 

- 참조 축약을 이용한 std::forward 의 동작 원리

 

std::forward가 정상적으로 동작하는 이유는 참조 축약 덕분이라고 할 수 있다. 

 

간단히 예시를 들어보면 다음과 같다.

template<typename T>
T&& Forward(std::remove_reference_t<T>& param)
{
	return static_cast<T&&>(param);
}

template <typename T>
void Function(T&& param)
{
	FunctionImpl(Forward<T>(param));
}

Function에 전달된 인수 param가 왼값인지 오른값인지에 대한 정보가 형식 매개변수 T에 부호화된다.

 

T가 비참조 형식이면 오른값이고 왼값 param을 오른값으로 캐스팅한다. 상황에 따라 캐스팅되는 형식을 표로 나타내면 다음과 같다.

T param 전달된 인수 타입 Forward 캐스팅
Type Type&& 오른값 Type&&(오른값)
Type& Type& 왼값 Type& && => Type&(왼값)

 

 

- auto 변수에 대한 형식연역에서의 참조 축약

 

auto 변수의 형식 연역은 템플릿의 형석 연역과 본질적으로 같기 때문에 참조 축약이 일어난다.

template <typename T>
void Function(T&& param)
{
	// ...
}

void AutoTest(void)
{
	int a = 3;

	// int&로 연역 // int& && -> int&
	Function(a);

	// 템플릿 연역과 본질적으로 같다. 따라서 int&로 연역 // int& && -> int&
	auto&& b = a;
}

 

 

- typedef와 별칭 선언에서의 참조 축약

 

typedef와 별칭 선언에서 평가되는 도중에 참조에 대한 참조가 발생하면 참조 축약이 끼어들어서 참조에 대한 참조를 제거한다.

template <typename T>
class Widget
{
public:
	typedef T&& RValueRefToT1;
	using RValueRefToT2 = typename T&&;
};

void DefTest(void)
{
	// int& // int& && -> int&
	Widget<int&>::RValueRefToT1;

	// int& // int& && -> int&
	Widget<int&>::RValueRefToT2;

	// int&&
	Widget<int>::RValueRefToT1;

	// int&&
	Widget<int>::RValueRefToT2;
}

 

 

- decltype에서의 참조 축약

 

컴파일러가 decltype에 관여갛는 형식을 분석하는 도중에 참조에 대한 참조가 존재하면 참조 축약이 발생한다.

 

 

 

'C++' 카테고리의 다른 글

람다(Lambda)  (0) 2022.04.11
완벽 전달(Perfect forwarding)의 실패  (0) 2022.03.31
보편 참조에 대한 중복적재 대안  (0) 2022.03.31
보편 참조와 오른값 참조  (0) 2022.03.27
std::forward  (0) 2022.03.27

+ Recent posts