* 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

* 정의

 

람다 표현식(lambda expression)(람다(lambda))는 호출되거나 함수에 대한 인수로 전달되는 위치에서 익명 함수 객체를 정의하는 편리한 방법이다.

 

 

* 사용 이유

 

알고리즘 또는 비동기 함수들에 전달되는 몇 줄의 코드를 캡슐화하는 데 사용된다.

 

다음과 같은 곳에서 유용하게 사용된다.

  • _if 알고리즘 : std::find_if, std::remove_if, std::count_if
  • 비교 함수로 커스텀화할 수 있는 알고리즘 : std::sort, std::nth_element, std::lower_bound
  • std::unique_ptr, std::shared_ptr를 위한 삭제자
  • 콜백 함수
  • 인터페이스 적응 함수
  • 일회성 호출을 위한 문맥 국한적 함수

 

 

* 동작 방식

 

- 용어 정리

 

* 람다 표현식(lambda expression)

 : 하나의 표현식으로 소스 코드의 일부이다.

 

* 클로저(closure)

 : 람다에 의해 만들어진 실행시점 객체이다. 캡처 모드(capture mode)에 따라 클로저가 갈무리된 자료의 복사본을 가질 수도 있고 그 자료에 대한 참조를 가질 수도 있다.

 

* 클로저 클래스

 : 클로저를 만드는 데 쓰인 클래스를 말한다. 각각의 람다에 대해 컴파일러는 고유한 클로저 클래스를 만들어 낸다. 람다 안의 문장들은 해당 클로저 클래스의 멤버 함수들 안의 실행 가능한 명령들이 된다.

 

void Lambda_Test(void)
{
	int x = 3;

	// c1은 람다에 의해 만들어진 클로저의 복사본
	auto c1 = [x](int y) { return x * y; };

	// c2는 c1의 복사본
	auto c2 = c1;
}

 

 

- 간단한 동작 방식

 

람다 표현식은 컴파일러가 하나의 클래스(클로저 클래스)를 자동으로 작성해서 그 클래스의 객체(클로저)를 생성하게 만든다.

 

 

* 기본 캡처 모드(default capture mode)

 

C++11의 기본 캡처 모드(default capture mode)는 다음과 같이 두 가지가 있다.

  • 참조에 의한(by-reference) 캡처 모드
  • 값에 의한(by-value) 캡처 모드

기본 참조 캡처 모드에서는 참조가 대상을 잃는 문제가 있을 수 있고, 기본 값 캡처 모드에서는 자기 완결적(self-contained)이지 않을 수 있다. 

 

 

- 기본 참조 캡처 주의 사항

 

참조 캡처를 사용하는 클로저는 지역 변수 또는 람다가 정의된 범위에서 볼 수 있는 매개변수에 대한 참조를 가지게 된다. 하지만 다음과 같은 코드에서는 참조가 대상을 잃는 문제가 발생한다.

std::vector<std::function<bool(int)>> Filters;

void AddDivisorFilter(void)
{
	// ...
	int divisor = Compute();

	// 참조 캡처를 사용하는 과정에서 divisor에 대한 참조가 대상을 잃는다.
	//Filters.emplace_back([&](int value) { return (value % divisor) == 0; });
	Filters.emplace_back([&divisor](int value) { return (value % divisor) == 0; });
}

void Lambda_CaptureMode(void)
{
	std::vector<std::function<bool(int)>> filters;

	AddDivisorFilter(filters);
	
	// 추가 하는 과정에서의 참조 대상 divisor는 지역변수이기 때문에 여기서는 유효하지 않다.
	filters[0](2);
}

람다에 의해 생성된 클러저의 수명이 그 지역 변수나 매개변수의 수명보다 길기 때문에 클로저 안의 참조는 대상을 잃는 현상이 발생한 것이다. [&], [&divisor] 어떤 형식을 사용하여도 마찬가지이다. 하지만 [&divisor]과 같이 명시적으로 써주면 람다의 클로저의 유효성을 좀 더 직관적으로 판단할 수 있다. (divisor가 람다의 클로저 이상으로 오래 살아있어야 한다고 직관적으로 알 수 있다.)

 

 

- 기본 값 캡처 주의 사항

 

위의 참조 캡처의 문제점을 해결하기 위해 다음과 같이 기본 값 캡처 모드를 사용할 수 있다.

void AddDivisorFilter(void)
{
	// ...
	int divisor = Compute();

	Filters.emplace_back([=](int value) { return (value % divisor) == 0; });
}

 

 

@ 포인터에 대한 값 캡처

 

기본 값 캡처 방식은 완전한 해결책은 되지 못한다. 예를 들어 포인터를 값으로 캡처하면 포인터가 람다에 의해 생성된 클로저 안으로 복사되는데, 람다 바깥에서 그 포인터가 삭제될 수 있기 때문이다. 

 

또한 포인터의 값이 복사되었다는 것을 인지하기 어려운 경우도 존재한다. 예를 들어 클래스에서 람다를 사용하는 경우에 그렇다.

class Widget
{
public:
	void addFilter(void) const
	{
		Filters.emplace_back([=](int value) { return (value % _divisor) == 0; });
	}

public:
	int _divisor;
};

얼핏보면 _divisor의 값을 복사해서 사용하는 것처럼 생각할 수 있다. 하지만 캡처는 오직 람다가 생성된 범위 안에서 보이는 static이 아닌 지역 변수(매개변수 포함)에만 적용된다.

 

위의 코드에서 지역변수도 매개변수도 아닌 _divisor에 접근했는데 컴파일 되는 이유는 클래스 내부에서 암묵적으로 this라는 포인터를 사용하고 있기 때문이다. 위의 코드를 명시적으로 표현하면 다음과 같이 표현된다.

void addFilter(void) const
{
    auto currentObjectPtr = this;
    Filters.emplace_back([currentObjectPtr](int value) { return (value % currentObjectPtr->_divisor) == 0; });
}

만약 this를 가리키는 Widget 객체가 소멸한후 람다를 사용한다면 미정의 행동을 유발하게 된다.

 

이를 해결하기 위해서는 지역 변수에 멤버 변수의 값을 복사하고 지역 변수를 값으로 전달하면 된다. 또한 일반화된 람다 갈무리를 사용해서도 해결할 수 있다.

// 지역 변수에 복사한 후 값 캡처
void addFilter(void) const
{
	auto divisorCopy = _divisor;
	Filters.emplace_back([divisorCopy](int value) { return (value % divisorCopy) == 0; });
}

// 일반화된 람다 캡처
void addFilter(void) const
{
	Filters.emplace_back([divisor = _divisor](int value) { return (value % divisor) == 0; });
}

 

 

@ 자기 완결적으로 보이지만 그렇지 않을 수 있다.

 

void AddDivisorFilter(void)
{
	// ...
	static int divisor = Compute();

	Filters.emplace_back([=](int value) { std::cout << divisor << '\n'; return (value % divisor) == 0; });
	++divisor;
}

다음과 같은 코드를 보면 [=]에서 복사된다고 생각할 수 있다. 하지만 이 람다는 어떤 비정적 지역 변수도 사용하지 않기 때문에 캡처 할 수 없다. 캡처는 오직 람다가 생성된 범위 안에서 보이는 static이 아닌 지역 변수(매개변수 포함)에만 적용된다.

 

즉 람다의 코드는 static 변수 divisor을 가리키게 되고 해당 람다를 필터에 추가한 시점에서의 divisor값이 아닌 사용 시점의 divisor값을 사용하게 된다.

 

 

* 초기화 캡처(init capture)(일반화된 람다 캡처(generalized lambda capture))

 

이동 전용 객체(std::unique_ptr, std::future 등)의 경우에는 클로저 안으로 들어가야 하고, 복사는 비싸지만 이동은 저렴한 객체들 역시 클로저 안으로 들여오는 것이 좋다. C++14부터 객체를 클로저 안으로 이동하는 수단을 직접 제공한다.

 

이를 실현시키는 수단은 초기화 캡처(init capture)라고 불리는 방법이다. 이 방법은 C++11의 캡처에서 할 수 있는 모든 것들을 할 수 있으며 그 외 여러 가지 기능도 지원한다.

 

 

- 초기화 캡처(C++14)

 

초기화 캡처로 다음과 같은 것들을 지정할 수 있다.

  • 람다로부터 생성되는 클로저 클래스에 속한 자료 멤버의 이름
  • 그 자료 멤버를 초기화하는 표현식
void Lambda_InitCapture(void)
{
	auto pw = std::make_unique<Widget>();

	auto func = [pw = std::move(pw)]{ return pw->isValidated(); };
}

위와 같은 코드에서 좌변 pw는 클로저 클래스 안의 자료 멤버를 지칭하고 우변 pw은 람다 이전에 선언된 객체를 뜻한다. 이를 종합해보면 다음과 같다.

클로저 안에서 자료 멤버 pw를 생성하되, 지역 변수 pw에 std::move를 적용한 결과로 그 자료 멤버를 초기화 하라.

람다 본문의 코드는 클로저 클래스의 범위 안에 있으므로 본문에 있는 pw는 클로저 클래스의 해당 자료 멤버를 지칭한다.

 

이처럼 C++14에서는 어떤 표현식의 결과를 캡처할 수 있도록 지원하고 있는 것이다. 이를 일반화된 람다 캡처(generalized lambda capture)라고 부르기도 한다.

 

위의 람다식에 의해 생성되는 클래스는 다음과 비슷하게 만들어진다.

class ClosureClass
{
public:
	explicit ClosureClass(std::unique_ptr<Widget>&& ptr)
		: pw(std::move(ptr)) {}

	bool operator()() const
	{
		return pw->isValidated();
	}

private:
	std::unique_ptr<Widget> pw;
};

 

 

- 초기화 캡처 흉내(C++11)

 

C++11에서 이를 흉내내기 위해서는 다음과 같은 방식을 사용해야한다.

  • 캡처할 객체를 std::bind가 산출하는 함수 객체로 이동시킨다.
  • 캡처 객체에 대한 참조를 람다에 넘겨준다.

 

C++14와 비교해서 보면 다음과 같다.

void Lambda_InitCaptureCopy(void)
{
	std::vector<double> data;
	// ...
	
	// C++14
	auto func1 = [data = std::move(data)]{ /* data[index] ... */ };

	// C++11
	auto func2 = std::bind([](const std::vector<double>& data) { /* data[index] ... */ },
						   std::move(data));
}

 

위의 상황을 그림으로 다음과 같이 나타낼 수 있다.

 

여기서 다음과 같은 사실을 확인할 수 있다.

 

  • std::bind는 호출 가능한 함수 객체(바인드 객체)를 돌려준다.
  • std::bind의 첫 인수는 호출 가능한 객체이고, 나머지 인수는 그 객체에 전달할 값들을 나타낸다.
  • std::bind에 전달된 모든 인수의 복사본들을 포함한다.
  • 람다가 생성한 클로저 또한 복사되어 바인드 객체에 저장된다.
  • C++11에서는 C++14에서 처럼 클로저 안으로 객체를 이동 생성하는 것이 불가능하기 때문에 바인드 객체에 저장한다.
  • 바인드 객체에 저장된 객체는 클로저에 참조로 전달한다.
  • 복사된 클로저와 바인드 객체의 수명은 동일하기 때문에 바인드 객체 안의 객체들을 마치 클로저 안에 있는 것처럼 취급할 수 있다. 
  • std::bind는 전달된 인수에 대하여 왼값은 복사 생성된 객체, 오른값은 이동 생성된 객체를 가지고 있다.
  • 람다로부터 만들어진 클로저 클래스의 operator(), 모든 자료 멤버는 const이다.
  • 바인드 객체 안의 이동 생성된 data 복사본은 const가 아니다. 만약 data를 변경해야한다면 클로저의 operator함수를 non-const함수로 변경할 필요가 있다. 이는 람다를 작성할 때 mutable키워드를 붙여서 작성함으로써 실현된다.

 

 

* 람다의 매개변수와 auto : 일반적 람다(generic lambda)

 

람다 표현식의 매개변수에도 auto 키워드를 사용할 수 있다. 이를 일반적 람다(generic lambda)라고 한다.

일반적인 auto를 사용한 람다 표현식은 다음과 같다.

auto f1 = [](auto x) { return normalize(x); };

 

 

만약 완벽 전달을 수행하려면 보편 참조 형식으로 바꿔주면 된다. 다만 매개변수를 다른 곳으로 넘겨줄 때 템플릿에서의 보편참조와 같이 std::forward<T>를 사용하지 못한다. (T라는 것을 알지 못하기 때문에)

 

하지만 그냥 자료형 그대로 넘겨주어도 문제가 없다. 참조 축약 규칙에 의해 오른값(int&& 등)을 넘겨주어도 오른값(int&& 등)을 돌려받는다.

auto f2 = [](auto&& x) { return normalize(std::forward<decltype(x)>(x)); };

 

 

여기서 많은 가변 매개변수를 받고 싶다면 다음과 같이 수정하면 된다.

auto f3 = [](auto&&... xs) { return normalize(std::forward<decltype(xs)>(xs)...); };

 

 

* 람다 vs std::bind

 

결론부터 말하자면 람다가 가독성, 편의성 등 모든 면에서 뛰어나다고 볼 수 있다.

 

예시로 어떤 지점에서 한 시간 후부터 30초간 소리를 내는 함수가 있다고 가정한다.

enum class Sound { Beep, Siren, Whistle };
using Time = std::chrono::steady_clock::time_point;
using Duration = std::chrono::steady_clock::duration;


void SetAlarm(Time t, Sound s, Duration d) { /* ... */ }

void Lambda_Bind(void)
{
	auto setSoundByLambda = [](Sound s)
	{
		using namespace std::chrono;
		using namespace std::literals;

		SetAlarm(steady_clock::now() + 1h,
				 s,
				 30s);
	};

	using namespace std::chrono;
	using namespace std::literals;
	using namespace std::placeholders;

	auto setSoundByBind =
		std::bind(SetAlarm,
				  steady_clock::now() + 1h,
				  _1,
				  30s);
}

 

람다와 바인드를 사용해 구현하면 위와 같이 구현된다.

 

- 람다는 바인드에 비해 명확하고 가독성이 좋다.

 

바인드는 자리표(placeholder)라는 것을 통해 setSoundByBind의 첫인수를 SetAlarm의 두번째 인수로 전달하는 역할을 한다. 바인드 객체를 통해 인수가 어떤 SetAlarm의 어떤 매개변수로 들어가는지 신경써야한다. 람다는 간단히 봐도 정확하게 파악할 수 있다.

 

한가지 예를 들어보자면 다음과 같다.

void Lambda_Bind(void)
{
	int lowVal = 3;
	int highVal = 10;

	auto betweenByLambda = [lowVal, highVal](const auto& val)
	{ return lowVal <= val && val <= highVal; };

	using namespace std::placeholders;
	auto betweenByBind =
		std::bind(std::logical_and<bool>(),
				  std::bind(std::less_equal<int>(), lowVal, _1),
				  std::bind(std::less_equal<int>(), _1, highVal));

}

 

 

- 바인드 사용 시, 바인드 시점에 값이 결정되지 않도록 지연시킬 필요가 있다.

 

위의 코드에서 람다는 setSoundByLambda를 호출할 때 시간이 계산되어 한 시간 후 부터 30초간 알람이 울린다. 하지만 바인드의 경우 std::bind를 호출할 때 시간이 계산되어 이 시점기준으로 한 시간 후 30초간 알람이 울리게 된다. 이를 방지하기 위해서 다음과 같은 코드를 작성해야한다. 

	auto setSoundByBind =
		std::bind(SetAlarm,
				  std::bind(std::plus</*steady_clock::time_point*/>(),
							std::bind(steady_clock::now),
							1h),
				  _1,
				  30s);

 

 

- 바인드 사용 시, 함수의 이름 밖에 알지 못하기 때문에 중복적재된 함수를 적절하게 선택할 수 없다.

 

이를 명시적으로 std::bind에 알려줘야 한다.

void SetAlarm(Time t, Sound s, Duration d) { /* ... */ }
// 중복적재 추가
void SetAlarm(Time t, Sound s, Duration d, int factor) { /* ... */ }

void Lambda_Bind(void)
{
	// ...
	using namespace std::chrono;
	using namespace std::literals;
	using namespace std::placeholders;
	using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
    
	auto setSoundByBind =
		std::bind(static_cast<SetAlarm3ParamType>(SetAlarm),
				  std::bind(std::plus</*steady_clock::time_point*/>(),
							std::bind(steady_clock::now),
							1h),
				  _1,
				  30s);
}

 

 

- 바인드 사용 시, 함수 포인터를 통해 호출이 일어나기 때문에 컴파일러가 인라인화할 가능성이 더 낮다.

 

- 바인드 사용 시, 매개변수의 저장 방식(std::bind에서 매개변수로 받은 값들을 저장할 때)에 대해 알아야한다.(값으로 전달된다.) 람다에서는 캡처 방식에 따라서 명확하게 확인할 수 있다.

 

- 바인드 사용 시, 바인드 객체를 호출 시 인수의 전달 방식을 알야아 한다.(참조로 전달된다.)

 

 

바인드는 C++11에서만 가끔 쓰이는 경우가 있는데(C++14에서는 쓰이지 않는다.) 쓰이는 경우는 다음 두 가지이다.

  1. 이동 캡처 구현
  2. 다형적 함수 객체(다양한 인수들을 받을 수 있게 하기 위해)

 

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

가변인자 템플릿(Variadic Template)  (0) 2022.05.23
템플릿(Template)  (0) 2022.05.23
완벽 전달(Perfect forwarding)의 실패  (0) 2022.03.31
참조 축약  (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