수색…
소개
C ++에서 메타 프로그래밍이란 매크로 나 템플릿을 사용하여 컴파일 타임에 코드를 생성하는 것을 말합니다.
일반적으로 매크로는이 역할에서 눈살을 찌푸리게되며 템플릿은 일반적이지는 않지만 템플릿을 사용하는 것이 좋습니다.
템플릿 메타 프로그래밍은 종종 템플릿 생성이나 constexpr
함수를 통해 컴파일 타임 계산을 사용하여 코드 생성 목표를 달성하지만 컴파일 시간 계산은 그 자체로 메타 프로그래밍이 아닙니다.
비고
메타 프로그래밍 (또는보다 구체적으로는 템플릿 메타 프로그래밍)은 템플릿 을 사용하여 컴파일 타임에 상수, 함수 또는 데이터 구조를 만드는 관행입니다. 이를 통해 각 실행 시간보다는 컴파일 시간에 한 번 계산을 수행 할 수 있습니다.
Factorials 계산하기
팩토 리얼은 템플릿 메타 프로그래밍 기술을 사용하여 컴파일 타임에 계산할 수 있습니다.
#include <iostream>
template<unsigned int n>
struct factorial
{
enum
{
value = n * factorial<n - 1>::value
};
};
template<>
struct factorial<0>
{
enum { value = 1 };
};
int main()
{
std::cout << factorial<7>::value << std::endl; // prints "5040"
}
factorial
은 구조체이지만 템플릿 메타 프로그래밍에서는 템플릿 메타 기능으로 처리됩니다. 규칙에 따라, 템플리트 메타 기능은 특정 구성원을 점검하여 ::type
을 판별하는 메타 기능에 대해 ::type
을 지정하거나 ::value
을 생성하는 메타 기능에 대해 ::value
를 평가하여 평가합니다.
위의 코드에서 우리는 전달하고자하는 매개 변수로 템플릿을 인스턴스화하고 ::value
를 사용하여 평가 결과를 얻는 방법으로 factorial
함수를 평가합니다.
메타 함수 자체는 더 작은 값으로 동일한 메타 함수를 재귀 적으로 인스턴스화하는 것에 의존합니다. factorial<0>
특수화는 종료 조건을 나타냅니다. 템플릿 메타 프로그래밍에는 함수형 프로그래밍 언어 의 제한 사항이 대부분 있으므로 재귀는 기본 "루핑"구문입니다.
템플리트 메타 기능은 컴파일시에 실행되므로 컴파일 시간 값이 필요한 컨텍스트에서 그 결과를 사용할 수 있습니다. 예 :
int my_array[factorial<5>::value];
자동 배열에는 컴파일시 정의 된 크기가 있어야합니다. 그리고 메타 함수의 결과는 컴파일 타임 상수이기 때문에 여기에서 사용할 수 있습니다.
제한 : 대부분의 컴파일러는 한도를 초과하는 재귀 깊이를 허용하지 않습니다. 예를 들어 g++
컴파일러는 기본적으로 재귀 호출을 256 단계로 제한합니다. g++
경우 프로그래머는 -ftemplate-depth-X
옵션을 사용하여 재귀 깊이를 설정할 수 있습니다.
C ++ 11 이후, std::integral_constant
템플릿은 이런 종류의 템플릿 계산에 사용될 수 있습니다 :
#include <iostream>
#include <type_traits>
template<long long n>
struct factorial :
std::integral_constant<long long, n * factorial<n - 1>::value> {};
template<>
struct factorial<0> :
std::integral_constant<long long, 1> {};
int main()
{
std::cout << factorial<7>::value << std::endl; // prints "5040"
}
또한 constexpr
함수는 더 깨끗한 대안이됩니다.
#include <iostream>
constexpr long long factorial(long long n)
{
return (n == 0) ? 1 : n * factorial(n - 1);
}
int main()
{
char test[factorial(3)];
std::cout << factorial(7) << '\n';
}
factorial()
의 본문은 C ++ 11에서 constexpr
함수는 언어의 일부만 사용할 수 있기 때문에 단일 문으로 작성됩니다.
C ++ 14 이후로, constexpr
함수에 대한 많은 제한이 없어졌으며 이제는 훨씬 더 편리하게 작성할 수 있습니다 :
constexpr long long factorial(long long n)
{
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
또는:
constexpr long long factorial(int n)
{
long long result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
C ++ 17에서는 fold expression을 사용하여 계승을 계산할 수 있습니다.
#include <iostream>
#include <utility>
template <class T, T N, class I = std::make_integer_sequence<T, N>>
struct factorial;
template <class T, T N, T... Is>
struct factorial<T,N,std::index_sequence<T, Is...>> {
static constexpr T value = (static_cast<T>(1) * ... * (Is + 1));
};
int main() {
std::cout << factorial<int, 5>::value << std::endl;
}
매개 변수 팩 반복
종종 가변성 템플릿 매개 변수 팩의 모든 요소에 대해 연산을 수행해야합니다. 이를 수행 할 수있는 많은 방법이 있으며 솔루션은 C ++로 읽기 및 쓰기가 더 쉽습니다. 팩의 모든 요소를 인쇄하기 만하면됩니다. 가장 간단한 해결책은 재발하는 것입니다.
void print_all(std::ostream& os) {
// base case
}
template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
os << first;
print_all(os, rest...);
}
대신 단일 기능으로 모든 스트리밍을 수행하기 위해 확장기 트릭을 사용할 수 있습니다. 이것은 두 번째 과부하가 필요 없다는 장점이 있지만 별 읽기가 쉽지 않은 단점이 있습니다.
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
using expander = int[];
(void)expander{0,
(void(os << args), 0)...
};
}
이것이 어떻게 작동하는지에 대한 설명은 TC의 탁월한 답을 참조하십시오.
C ++ 17에서 우리는이 문제를 해결하기 위해 무기고에 두 가지 강력한 새로운 도구를 제공합니다. 첫 번째는 fold-expression입니다.
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
((os << args), ...);
}
그리고 두 번째는 if constexpr
입니다. 이렇게하면 우리는 원래의 재귀 적 솔루션을 단일 함수로 작성할 수 있습니다 :
template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
os << first;
if constexpr (sizeof...(rest) > 0) {
// this line will only be instantiated if there are further
// arguments. if rest... is empty, there will be no call to
// print_all(os).
print_all(os, rest...);
}
}
std :: integer_sequence로 반복하기
C ++ 14 이후, 표준은 클래스 템플릿을 제공합니다.
template <class T, T... Ints>
class integer_sequence;
template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;
그것에 대한 생성 메타 기능 :
template <class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;
이것은 C ++ 14에서 표준으로 제공되지만 C ++ 11 도구를 사용하여 구현할 수 있습니다.
우리는이 도구를 사용하여 std::tuple
인수 ( std::apply
처럼 C ++ 17에서 표준화 됨)가있는 함수를 호출 할 수 있습니다.
namespace detail {
template <class F, class Tuple, std::size_t... Is>
decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) {
return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...);
}
}
template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tpl) {
return detail::apply_impl(std::forward<F>(f),
std::forward<Tuple>(tpl),
std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}
// this will print 3
int f(int, char, double);
auto some_args = std::make_tuple(42, 'x', 3.14);
int r = apply(f, some_args); // calls f(42, 'x', 3.14)
태그 디스패치
컴파일 타임에 함수들 사이를 선택하는 간단한 방법은 태그를 하나의 인수 (대개 마지막 인자)로 사용하는 오버로드 된 함수 쌍에 함수를 전달하는 것입니다. 예를 들어, std::advance()
를 구현하기 위해 iterator 카테고리를 디스패치 할 수있다.
namespace details {
template <class RAIter, class Distance>
void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {
it += n;
}
template <class BidirIter, class Distance>
void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {
if (n > 0) {
while (n--) ++it;
}
else {
while (n++) --it;
}
}
template <class InputIter, class Distance>
void advance(InputIter& it, Distance n, std::input_iterator_tag) {
while (n--) {
++it;
}
}
}
template <class Iter, class Distance>
void advance(Iter& it, Distance n) {
details::advance(it, n,
typename std::iterator_traits<Iter>::iterator_category{} );
}
오버로드 된 details::advance
std::XY_iterator_tag
인수 details::advance
함수는 사용되지 않는 함수 매개 변수입니다. 실제 구현은 중요하지 않습니다 (실제로는 완전히 비어 있음). 그들의 유일한 목적은 컴파일러가 태그 클래스 details::advance
가 호출되는 것을 기반으로 오버로드를 선택할 수있게하는 것입니다.
이 예제에서 advance
는 Iter
의 실제 유형에 따라 iterator_tag
클래스 중 하나를 반환하는 iterator_traits<T>::iterator_category
메타 함수를 사용합니다. iterator_category<Iter>::type
의 기본 생성 개체를 사용하면 컴파일러에서 details::advance
의 여러 오버로드 중 하나를 선택할 수 있습니다. (이 함수 매개 변수는 빈 struct
의 기본 생성 객체이며 사용되지 않으므로 완벽하게 최적화 될 가능성이 큽니다.
태그를 파견하면 SFINAE 및 enable_if
사용하여 동일한 코드보다 훨씬 쉽게 읽을 수있는 코드를 얻을 수 있습니다.
참고 : C ++ 17에서는 if constexpr
이 특히 advance
구현을 단순화 할 수 있지만 태그 디스패치와 달리 개방 구현에는 적합하지 않습니다.
표현식이 유효한 지 여부 감지
연산자 나 함수를 호출 할 수 있는지 여부를 감지 할 수 있습니다. 클래스에 std::hash
의 오버로드가 있는지 테스트하려면 다음을 수행하십시오.
#include <functional> // for std::hash
#include <type_traits> // for std::false_type and std::true_type
#include <utility> // for std::declval
template<class, class = void>
struct has_hash
: std::false_type
{};
template<class T>
struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())>
: std::true_type
{};
C ++ 17 이후에 std::void_t
를 사용하여 이러한 유형의 구조를 단순화 할 수 있습니다.
#include <functional> // for std::hash
#include <type_traits> // for std::false_type, std::true_type, std::void_t
#include <utility> // for std::declval
template<class, class = std::void_t<> >
struct has_hash
: std::false_type
{};
template<class T>
struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > >
: std::true_type
{};
여기서 std::void_t
는 다음과 같이 정의됩니다.
template< class... > using void_t = void;
operator<
와 같은 operator<
가 정의되었는지 여부를 감지하기 위해 구문은 거의 동일합니다.
template<class, class = void>
struct has_less_than
: std::false_type
{};
template<class T>
struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())>
: std::true_type
{};
T
가 std::hash
과부하가 걸리면 std::unordered_map<T>
을 사용하지만 그렇지 않으면 std::map<T>
을 사용하려고 시도하는 데 사용할 수 있습니다.
template <class K, class V>
using hash_invariant_map = std::conditional_t<
has_hash<K>::value,
std::unordered_map<K, V>,
std::map<K,V>>;
C ++ 11 이상의 전력 계산
컴파일시 C ++ 11 이상의 계산은 훨씬 쉬울 수 있습니다. 예를 들어 컴파일 타임에 주어진 숫자의 거듭 제곱을 계산하는 것은 다음과 같습니다 :
template <typename T>
constexpr T calculatePower(T value, unsigned power) {
return power == 0 ? 1 : value * calculatePower(value, power-1);
}
키워드 constexpr
은 컴파일 시간에 함수를 계산하는 기능을 담당합니다.이 때 모든 요구 사항이 충족 될 때만 (예 : constexpr 키워드 참조) 컴파일 타임에 모든 인수를 알아야합니다.
참고 : C ++ 11에서 constexpr
함수는 return 문 하나만 작성해야합니다.
장점 : 컴파일 타임 계산의 표준 방식과 비교하여,이 방법은 런타임 계산에도 유용합니다. 즉, 함수의 인수가 컴파일 타임에 알려지지 않은 경우 (예 : 값 및 전력이 사용자를 통해 입력으로 제공됨) 함수가 컴파일 시간에 실행되므로 코드를 복제 할 필요가 없습니다. C ++의 구형 표준에서 강제 될 것이다).
예 :
void useExample() {
constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,
// as both arguments are known at compilation time
// and used for a constant expression.
int value;
std::cin >> value;
int runtimeCalculated = calculatePower(value, 3); // runtime calculated,
// because value is known only at runtime.
}
컴파일 타임에 전력을 계산하는 또 다른 방법은 다음과 같이 fold expression을 사용할 수 있습니다.
#include <iostream>
#include <utility>
template <class T, T V, T N, class I = std::make_integer_sequence<T, N>>
struct power;
template <class T, T V, T N, T... Is>
struct power<T, V, N, std::integer_sequence<T, Is...>> {
static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));
};
int main() {
std::cout << power<int, 4, 2>::value << std::endl;
}
어떤 타입이 주어 졌을 때 타입의 수동 구분 T
std::enable_if
enable_if를 사용하여 SFINAE 를 구현할 때 주어진 유형 T
가 조건 세트와 일치하는지 확인하는 도우미 템플리트에 액세스하는 것이 유용한 경우가 많습니다.
이를 돕기 위해 표준은 이미 true
와 false
두 가지 유형 인 std::true_type
과 std::false_type
합니다.
다음 예제는 유형 T
가 포인터인지 여부를 감지하는 방법을 보여 주며 is_pointer
템플릿은 표준 std::is_pointer
헬퍼의 동작을 모방합니다.
template <typename T>
struct is_pointer_: std::false_type {};
template <typename T>
struct is_pointer_<T*>: std::true_type {};
template <typename T>
struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }
위의 코드에는 세 단계가 있습니다 (때로는 두 개만 필요합니다).
is_pointer_
의 첫 번째 선언은 기본 경우 이며std::false_type
에서 상속됩니다. 디폴트의 경우는 "false
조건"과 유사하기 때문에std::false_type
으로부터 상속되어야한다.두 번째 선언은
T
가 실제로 무엇인지 신경 쓰지 않고 포인터T*
대한is_pointer_
템플릿을 전문화합니다. 이 버전은std::true_type
에서 상속받습니다.세 번째 선언 (실제)은
T
에서 불필요한 정보를 제거하기const
(이 경우에는const
및volatile
한정자를 제거합니다). 그런 다음 두 이전 선언 중 하나로 돌아갑니다.
is_pointer<T>
는 클래스이므로 값에 액세스하려면 다음 중 하나를 수행해야합니다.
-
::value
사용하십시오::value
예 :is_pointer<int>::value
-value
는std::true_type
또는std::false_type
에서 상속 된bool
유형의 정적 클래스 멤버입니다. -
is_pointer<int>{}
- 이것은std::is_pointer
가std::true_type
또는std::false_type
(constexpr
생성자를 가짐)과std::true_type
및std::false_type
에서 기본 생성자를 상속std::is_pointer
때문에 작동합니다std::false_type
constexpr
변환 연산자를bool
만듭니다.
값에 직접 액세스 할 수있게 해주는 "헬퍼 도우미 템플릿"을 제공하는 것이 좋은 습관입니다.
template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
C ++ 17 이상에서는 대부분의 도우미 템플릿이 이미 _v
버전을 제공합니다 (예 :
template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;
If-then-else
표준 라이브러리 헤더 <type_traits>
의 std::conditional
유형은 컴파일 타임 부울 값을 기반으로 한 유형 또는 다른 유형을 선택할 수 있습니다.
template<typename T>
struct ValueOrPointer
{
typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};
이 구조체에 대한 포인터를 포함 T
하면 T
포인터의 크기 또는보다 큰 T
는 포인터의 크기보다 작거나 같은 경우 자체. 그러므로 sizeof(ValueOrPointer)
는 항상 < sizeof(void*)
입니다.
가변 인수 카운트가있는 일반 최소 / 최대
템플리트 메타 프로그래밍을 사용하여 다양한 숫자 유형과 임의의 인수 수를 허용하는 일반 함수 (예 : min
)를 작성할 수 있습니다. 이 함수는 두 개의 인수에 대해 min
을 선언하고 more에 대해 재귀 적으로 선언합니다.
template <typename T1, typename T2>
auto min(const T1 &a, const T2 &b)
-> typename std::common_type<const T1&, const T2&>::type
{
return a < b ? a : b;
}
template <typename T1, typename T2, typename ... Args>
auto min(const T1 &a, const T2 &b, const Args& ... args)
-> typename std::common_type<const T1&, const T2&, const Args& ...>::type
{
return min(min(a, b), args...);
}
auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);