C++
Metaprogramming
खोज…
परिचय
C ++ में Metaprogramming का मतलब संकलन-समय पर कोड जनरेट करने के लिए मैक्रोज़ या टेम्प्लेट के उपयोग से है।
सामान्य तौर पर, मैक्रोज़ को इस भूमिका में रखा जाता है और टेम्पलेट्स को पसंद किया जाता है, हालाँकि वे उतने सामान्य नहीं हैं।
टेम्प्लेट मेटाप्रोग्रामिंग अक्सर कंपाइल-टाइम कम्प्यूटेशंस का उपयोग करता है, चाहे वह टेम्प्लेट या constexpr
फ़ंक्शंस के माध्यम से, कोड उत्पन्न करने के अपने लक्ष्यों को प्राप्त करने के लिए, हालांकि कंपाइल-टाइम कम्प्यूटेशंस प्रति मेटाप्रोग्रामिंग नहीं हैं।
टिप्पणियों
Metaprogramming (या अधिक विशेष रूप से, खाका Metaprogramming) संकलन-कार्यों, कार्यों या डेटा संरचनाओं को संकलन-समय पर बनाने के लिए टेम्पलेट्स का उपयोग करने का अभ्यास है। यह गणना को प्रत्येक रन समय के बजाय एक बार संकलन समय पर निष्पादित करने की अनुमति देता है।
तथ्य की गणना
टेम्पलेट मेटाप्रोग्रामिंग तकनीकों का उपयोग करके संकलन-समय पर फैक्टिरियल्स की गणना की जा सकती है।
#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
, जिसके परिणामस्वरूप ::type
होते हैं, या ::value
बनाने वाले मेटाफ़ंक्शन के लिए ::value
।
उपरोक्त कोड में, हम उन मापदंडों के साथ टेम्प्लेट को बदलकर factorial
मेटाफ़ंक्शन का मूल्यांकन करते हैं, जो हम पास करना चाहते हैं, और मूल्यांकन का परिणाम प्राप्त करने के लिए ::value
का उपयोग करते हैं।
मेटाफ़ंक्शन ही छोटे मूल्यों के साथ एक ही मेटाफ़ंक्शन को पुनरावृत्ति पर निर्भर करता है। 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 के बाद से गुट की गणना के लिए गुना अभिव्यक्ति का उपयोग कर सकते हैं:
#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;
}
एक पैरामीटर पैक पर Iterating
अक्सर, हमें वैरिएडिक टेम्प्लेट पैरामीटर पैक में प्रत्येक तत्व पर एक ऑपरेशन करने की आवश्यकता होती है। ऐसा करने के कई तरीके हैं, और समाधानों को C ++ 17 के साथ पढ़ना और लिखना आसान हो जाता है। मान लीजिए हम बस एक पैक में हर तत्व को प्रिंट करना चाहते हैं। सबसे सरल उपाय पुनरावृत्ति करना है:
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)...
};
}
यह कैसे काम करता है, इसकी व्याख्या के लिए, टीसी का उत्कृष्ट उत्तर देखें ।
C ++ 17 के साथ, हमें इस समस्या को हल करने के लिए हमारे शस्त्रागार में दो शक्तिशाली नए उपकरण मिलते हैं। पहला एक गुना-अभिव्यक्ति है:
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 के साथ Iterating :: पूर्णांक_ परिणाम
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
(के रूप में सी में मानकीकृत ++ 17 तर्कों के std::apply
):
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()
लागू करने के लिए, हम पुनरावृत्ति श्रेणी पर भेज सकते हैं:
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{} );
}
std::XY_iterator_tag
अतिभारित details::advance
std::XY_iterator_tag
तर्क details::advance
फ़ंक्शन अप्रयुक्त फ़ंक्शन पैरामीटर हैं। वास्तविक कार्यान्वयन कोई फर्क नहीं पड़ता (वास्तव में यह पूरी तरह से खाली है)। उनका एकमात्र उद्देश्य संकलक को एक अधिभार का चयन करने की अनुमति देना है जिसके आधार पर टैग वर्ग details::advance
को कहा जाता है।
इस उदाहरण में, advance
का उपयोग करता iterator_traits<T>::iterator_category
metafunction जिनमें से एक रिटर्न iterator_tag
कक्षाएं, की वास्तविक प्रकार के आधार पर Iter
। iterator_category<Iter>::type
एक डिफ़ॉल्ट-निर्मित ऑब्जेक्ट कंपाइलर details::advance
के विभिन्न अधिभार में से एक का चयन करने देता है 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<
परिभाषित किया गया है, वाक्यविन्यास लगभग समान है:
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
{};
इनका उपयोग एक std::unordered_map<T>
का उपयोग करने के लिए किया जा सकता है यदि T
में std::hash
लिए अधिभार है, लेकिन अन्यथा std::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
संकलन समय में फ़ंक्शन की गणना के लिए जिम्मेदार है, तब और केवल तब, जब इसके लिए सभी आवश्यकताओं को पूरा किया जाएगा (कॉन्स्ट्रेक्स कीवर्ड संदर्भ में अधिक देखें) उदाहरण के लिए सभी तर्कों को संकलन समय पर जाना जाना चाहिए।
नोट: C ++ 11 में constexpr
फ़ंक्शन को केवल एक रिटर्न स्टेटमेंट से रचना करना चाहिए।
लाभ: इसे संकलन समय गणना के मानक तरीके से तुलना करते हुए, यह विधि रनटाइम गणना के लिए भी उपयोगी है। इसका मतलब है, कि यदि फ़ंक्शन के तर्कों को संकलन समय पर नहीं जाना जाता है (जैसे मूल्य और शक्ति उपयोगकर्ता के माध्यम से इनपुट के रूप में दी जाती है), तो फ़ंक्शन संकलन समय में चलाया जाता है, इसलिए किसी कोड को डुप्लिकेट करने की आवश्यकता नहीं है (जैसा कि हम) 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.
}
संकलन समय पर बिजली की गणना करने का एक और तरीका निम्नानुसार गुना अभिव्यक्ति का उपयोग कर सकता है:
#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;
}
किसी भी प्रकार टी दिए जाने पर प्रकार का मैनुअल भेद
जब लागू करने SFINAE का उपयोग कर std::enable_if
, यह अक्सर सहायक टेम्पलेट्स कि निर्धारित करता है कि किसी दिए गए प्रकार की पहुंच है के लिए उपयोगी है T
मापदंड का एक सेट से मेल खाता है।
उस के साथ हमारी मदद करने के लिए, मानक पहले से ही true
और false
दो प्रकार के एनालॉग प्रदान करता है जो std::true_type
और std::false_type
।
निम्न उदाहरण दिखाता है कि कैसे पता लगाया जा सकता है कि टाइप T
एक पॉइंटर है या नहीं, is_pointer
टेम्पलेट मानक std::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
से विरासत मेंstd::false_type
। डिफ़ॉल्ट मामले को हमेशाstd::false_type
से प्राप्त करना चाहिएstd::false_type
क्योंकि यह "false
स्थिति" के अनुरूप है।दूसरा घोषणा सूचक
T*
लिएis_pointer_
टेम्पलेट विशेषज्ञ हैT*
T
वास्तव में क्या है, इसकी परवाह किए बिना। यह संस्करणstd::true_type
से इनहेरिटstd::true_type
।तीसरी घोषणा (वास्तविक एक) बस
T
से किसी भी अनावश्यक जानकारी को हटा दें (इस मामले में हमconst
औरvolatile
क्वालिफायर हटाते हैं) और फिर पिछली दो घोषणाओं में से एक में वापस आते हैं।
चूंकि is_pointer<T>
एक वर्ग है, इसके मूल्य तक पहुँचने के लिए आपको या तो:
- उपयोग करें
::value
, उदाहरण के लिएis_pointer<int>::value
-value
std::true_type
से विरासत में मिली प्रकार केbool
का एक स्थिर वर्ग सदस्य हैstd::true_type
याstd::false_type
; - इस प्रकार के एक वस्तु का निर्माण, जैसे
is_pointer<int>{}
- क्योंकि यह काम करता हैstd::is_pointer
से विरासत में मिली उसके डिफ़ॉल्ट निर्माताstd::true_type
याstd::false_type
(जो हैconstexpr
कंस्ट्रक्टर्स) और दोनोंstd::true_type
औरstd::false_type
Haveconstexpr
करने के लिए रूपांतरण ऑपरेटरों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;
अगर तब या
मानक लाइब्रेरी शीर्ष लेख में std::conditional
प्रकार std::conditional
<type_traits>
एक संकलन समय बूलियन मान के आधार पर एक प्रकार या दूसरे का चयन कर सकता है:
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
और अधिक के लिए पुनरावर्ती घोषित करता है।
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);