खोज…


enable_if

std::enable_if SFINAE को ट्रिगर करने के लिए बूलियन स्थितियों का उपयोग करने के लिए एक सुविधाजनक उपयोगिता है। इसे इस प्रकार परिभाषित किया गया है:

template <bool Cond, typename Result=void>
struct enable_if { };

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

यही है, enable_if<true, R>::type R लिए एक उपनाम है, जबकि enable_if<false, T>::type बीमार-गठन है क्योंकि enable_if विशेषज्ञता type सदस्य प्रकार नहीं है।

std::enable_if का उपयोग टेम्पलेट्स को बाध्य करने के लिए किया जा सकता है:

int negate(int i) { return -i; }

template <class F>
auto negate(F f) { return -f(); }

यहाँ, अस्पष्टता negate(1) को अस्पष्टता के कारण विफल कर दिया जाएगा। लेकिन दूसरा अधिभार अभिन्न प्रकारों के लिए उपयोग करने का इरादा नहीं है, इसलिए हम जोड़ सकते हैं:

int negate(int i) { return -i; }

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }

अब, instantiating negate<int> के बाद से एक प्रतिस्थापन विफलता में परिणाम होगा !std::is_arithmetic<int>::value है false । SFINAE के कारण, यह एक कठिन त्रुटि नहीं है, इस उम्मीदवार को बस अधिभार सेट से हटा दिया जाता है। परिणामस्वरूप, negate(1) केवल एक ही व्यवहार्य उम्मीदवार होता है - जिसे तब कहा जाता है।

इसका उपयोग कब करना है

यह ध्यान में रखने योग्य है कि std::enable_if SFINAE के शीर्ष पर एक सहायक है, लेकिन यह नहीं है कि पहले स्थान पर SFINAE काम करता है। आइए std::size , यानी एक अधिभार सेट size(arg) समान कार्यक्षमता को लागू करने के लिए इन दो विकल्पों पर विचार करें जो कंटेनर या सरणी के आकार का उत्पादन करते हैं:

// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);

// implementation omitted
template<typename Cont>
struct is_sizeable;

// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);

यह मानते हुए कि is_sizeable उचित रूप से लिखा गया है, ये दोनों घोषणाएं SFINAE के संबंध में बिल्कुल समान होनी चाहिए। लिखने में कौन सा सबसे आसान है और एक नज़र में समीक्षा करना और समझना सबसे आसान है?

अब आइए विचार करें कि हम अंकगणित सहायकों को कैसे लागू करना चाहते हैं जो कि चारों ओर या मॉड्यूलर व्यवहार के पक्ष में हस्ताक्षरित पूर्णांक अतिप्रवाह से बचें। जो कहना है कि उदाहरण के लिए incr(i, 3) i += 3 के समान होगा, इस तथ्य के लिए कि परिणाम हमेशा परिभाषित किया जाएगा भले ही i int INT_MAX साथ एक int हो। ये दो संभावित विकल्प हैं:

// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
 
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
 
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);

एक बार फिर जो लिखना सबसे आसान है, और जो एक नज़र में समीक्षा करना और समझना सबसे आसान है?

std::enable_if की एक ताकत std::enable_if यह है कि यह कैसे रिफैक्टरिंग और एपीआई डिजाइन के साथ खेलता है। यदि is_sizeable<Cont>::value को प्रतिबिंबित करने के लिए है कि क्या cont.size() मान्य है, तो अभिव्यक्ति का उपयोग करना जैसा कि size1 लिए प्रकट होता है, अधिक संक्षिप्त हो सकता है, हालांकि यह निर्भर कर सकता है कि is_sizeable कई स्थानों पर उपयोग किया जाएगा या नहीं । इसके विपरीत जो std::is_signed जो इसके इरादे को तब और अधिक स्पष्ट रूप से दर्शाता है जब इसका कार्यान्वयन incr1 की घोषणा में लीक हो जाता है।

void_t

सी ++ 11

void_t एक मेटा-फंक्शन है जो void टाइप करने के लिए किसी भी (संख्या) प्रकारों को मैप करता है। void_t का प्राथमिक उद्देश्य टाइप लक्षणों के लेखन की सुविधा प्रदान करना है।

std::void_t C ++ 17 का हिस्सा होगा, लेकिन तब तक, यह लागू करने के लिए बहुत सीधा है:

template <class...> using void_t = void;

कुछ संकलक को थोड़ा अलग कार्यान्वयन की आवश्यकता होती है:

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type;

void_t का प्राथमिक अनुप्रयोग प्रकार के लक्षण लिख रहा है जो किसी कथन की वैधता की जांच करते हैं। उदाहरण के लिए, आइए देखें कि क्या किसी प्रकार में कोई सदस्य फ़ंक्शन foo() जो कोई तर्क नहीं लेता है:

template <class T, class=void>
struct has_foo : std::false_type {};

template <class T>
struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};

यह कैसे काम करता है? जब मैंने has_foo<T>::value को तत्काल करने की कोशिश की, तो संकलक के पास has_foo<T, void> लिए सर्वश्रेष्ठ विशेषज्ञता देखने की कोशिश करने का कारण होगा। हमारे पास दो विकल्प हैं: प्राथमिक, और यह द्वितीयक जो कि उस अंतर्निहित अभिव्यक्ति को त्वरित करने के लिए शामिल है:

  • अगर T एक सदस्य समारोह है foo() जो कुछ भी प्रकार है कि रिटर्न में परिवर्तित हो जाता है, तो void है, और विशेषज्ञता टेम्पलेट आंशिक आदेश नियमों के आधार पर प्राथमिक करने के लिए पसंद किया जाता है। तो has_foo<T>::value true होगा
  • अगर T इस तरह के एक सदस्य समारोह (या यह एक तर्क से भी अधिक की आवश्यकता है) नहीं है, तो प्रतिस्थापन विशेषज्ञता के लिए विफल रहता है और हम केवल पर वापस आने की प्राथमिक टेम्पलेट है। इसलिए, has_foo<T>::value false

एक सरल मामला:

template<class T, class=void>
struct can_reference : std::false_type {};

template<class T>
struct can_reference<T, std::void_t<T&>> : std::true_type {};

यह std::declval या decltype उपयोग नहीं करता है।

आप एक शून्य तर्क का एक सामान्य पैटर्न देख सकते हैं। हम इसे बाहर कर सकते हैं:

struct details {
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:
    std::false_type
  {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
    std::true_type
  {};
};

template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

जो std::void_t के उपयोग को छुपाता है और can_apply को एक संकेतक की तरह कार्य करता है कि क्या पहले टेम्पलेट तर्क के रूप में दिया गया प्रकार उस में अन्य प्रकारों को प्रतिस्थापित करने के बाद अच्छी तरह से बनता है। पिछले उदाहरणों को अब can_apply रूप में फिर से लिखा जा सकता है:

template<class T>
using ref_t = T&;

template<class T>
using can_reference = can_apply<ref_t, T>;    // Is T& well formed for T?

तथा:

template<class T>
using dot_foo_r = decltype(std::declval<T&>().foo());

template<class T>
using can_dot_foo = can_apply< dot_foo_r, T >;    // Is T.foo() well formed for T?

जो मूल संस्करणों की तुलना में सरल लगता है।

can_apply समान std लक्षणों के लिए पोस्ट-सी ++ 17 प्रस्ताव हैं।

void_t की उपयोगिता की खोज वाल्टर ब्राउन ने की थी। उन्होंने CppCon 2016 में इस पर एक अद्भुत प्रस्तुति दी।

फ़ंक्शन टेम्प्लेट में अनुगामी अनुवर्तन

सी ++ 11

वापसी प्रकार को निर्दिष्ट करने के लिए अनुगामी decltype का उपयोग करने के लिए विवश करने वाला एक कार्य है:

namespace details {
   using std::to_string;

   // this one is constrained on being able to call to_string(T)
   template <class T>
   auto convert_to_string(T const& val, int )
       -> decltype(to_string(val))
   {
       return to_string(val);
   }

   // this one is unconstrained, but less preferred due to the ellipsis argument
   template <class T>
   std::string convert_to_string(T const& val, ... )
   {
       std::ostringstream oss;
       oss << val;
       return oss.str();
   }
}

template <class T>
std::string convert_to_string(T const& val)
{
    return details::convert_to_string(val, 0);
}

यदि मैं एक तर्क के साथ convert_to_string() को कॉल करता हूं, जिसके साथ मैं to_string() को लागू कर सकता हूं, तो मेरे पास details::convert_to_string() लिए दो व्यवहार्य कार्य हैं details::convert_to_string() । पहले से रूपांतरण के बाद से पसंद किया जाता है 0 के int से रूपांतरण की तुलना में बेहतर अंतर्निहित रूपांतरण अनुक्रम है 0 करने के लिए ...

यदि मैं एक तर्क के साथ convert_to_string() को कॉल करता हूं जिससे मैं to_string() आह्वान नहीं कर सकता हूं, तो पहला फ़ंक्शन टेम्प्लेट decltype(to_string(val)) प्रतिस्थापन विफलता की ओर जाता है (कोई decltype(to_string(val)) )। नतीजतन, उस उम्मीदवार को ओवरलोड सेट से हटा दिया जाता है। दूसरा फ़ंक्शन टेम्प्लेट अप्रतिबंधित है, इसलिए इसे चुना गया है और हम इसके बजाय operator<<(std::ostream&, T) से चलते हैं। यदि वह अपरिभाषित है, तो हमारे पास लाइन oss << val पर एक टेम्प्लेट स्टैक के साथ एक कठिन संकलन त्रुटि है।

SFINAE क्या है

SFINAE का मतलब S ubstitution F ailure I s N ot A n E rror है। बीमार का गठन कोड है कि प्रतिस्थापन प्रकार (या मान) से परिणाम एक समारोह टेम्पलेट या एक वर्ग टेम्पलेट का दृष्टांत को एक कठिन संकलन त्रुटि नहीं है, यह केवल एक कटौती विफलता के रूप में व्यवहार किया जाता है।

फंक्शन सिटिंग टेम्प्लेट या क्लास टेम्प्लेट स्पेशलाइजेशन पर डिडक्शन की विफलता उस उम्मीदवार को विचार के सेट से हटा देती है - मानो उस असफल उम्मीदवार के पास शुरू करने के लिए मौजूद नहीं था।

template <class T>
auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }

template <class T, size_t N>
T* begin(T (&arr)[N]) { return arr; }

int vals[10];
begin(vals); // OK. The first function template substitution fails because
             // vals.begin() is ill-formed. This is not an error! That function
             // is just removed from consideration as a viable overload candidate,
             // leaving us with the array overload. 

तत्काल संदर्भ में केवल प्रतिस्थापन विफलताओं को कटौती विफलता माना जाता है, अन्य सभी को कठिन त्रुटियां माना जाता है।

template <class T>
void add_one(T& val) { val += 1; }

int i = 4;
add_one(i); // ok

std::string msg = "Hello";
add_one(msg); // error. msg += 1 is ill-formed for std::string, but this
              // failure is NOT in the immediate context of substituting T

enable_if_all / enable_if_any

सी ++ 11

प्रेरक उदाहरण


जब आपके पास टेम्पलेट पैरामीटर सूची में एक वैरेडिक टेम्प्लेट पैक होता है, जैसे निम्न कोड स्निपेट:

template<typename ...Args> void func(Args &&...args) { //... };

मानक लाइब्रेरी (C ++ 17 से पहले) Args या Args किसी भी पैरामीटर पर सभी मानकों पर SFINAE बाधाओं को लागू करने के लिए enable_if लिखने का कोई सीधा तरीका नहीं Args । सी ++ 17 std::conjunction और std::disjunction प्रदान करता है जो इस समस्या को हल करते हैं। उदाहरण के लिए:

/// C++17: SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

/// C++17: SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

यदि आपके पास C ++ 17 उपलब्ध नहीं है, तो इन्हें प्राप्त करने के लिए कई समाधान हैं। उनमें से एक बेस-केस क्लास और आंशिक विशेषज्ञता का उपयोग करना है , जैसा कि इस प्रश्न के उत्तर में दिखाया गया है।

वैकल्पिक रूप से, एक हाथ से व्यवहार भी कर सकता है std::conjunction और std::disjunction सीधे-सीधे तरीके से। निम्नलिखित उदाहरण में, मैं कार्यान्वयन को प्रदर्शित करूँगा और उन्हें std::enable_if साथ संयोजित करके दो अन्य नाम उत्पन्न करने में enable_if_all : enable_if_all और enable_if_any , जो ठीक वही करते हैं जो उन्हें शब्दार्थ से माना जाता है। यह अधिक स्केलेबल समाधान प्रदान कर सकता है।


enable_if_all और enable_if_any का क्रियान्वयन


पहले चलो std::conjunction और std::disjunction का अनुकरण करें: क्रमशः अनुकूलित seq_and और seq_or का उपयोग करते हुए:

/// Helper for prior to C++14.
template<bool B, class T, class F >
using conditional_t = typename std::conditional<B,T,F>::type;

/// Emulate C++17 std::conjunction.
template<bool...> struct seq_or: std::false_type {};
template<bool...> struct seq_and: std::true_type {};

template<bool B1, bool... Bs>
struct seq_or<B1,Bs...>: 
  conditional_t<B1,std::true_type,seq_or<Bs...>> {};

template<bool B1, bool... Bs>
struct seq_and<B1,Bs...>:
  conditional_t<B1,seq_and<Bs...>,std::false_type> {};  

फिर कार्यान्वयन काफी सीधा-आगे है:

template<bool... Bs>
using enable_if_any = std::enable_if<seq_or<Bs...>::value>;

template<bool... Bs>
using enable_if_all = std::enable_if<seq_and<Bs...>::value>;

आखिरकार कुछ मददगार:

template<bool... Bs>
using enable_if_any_t = typename enable_if_any<Bs...>::type;

template<bool... Bs>
using enable_if_all_t = typename enable_if_all<Bs...>::type;

प्रयोग


उपयोग भी सीधे-आगे है:

    /// SFINAE constraints on all of the parameters in Args.
    template<typename ...Args,
             enable_if_all_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

    /// SFINAE constraints on any of the parameters in Args.
    template<typename ...Args,
             enable_if_any_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

is_detected

Type_trait निर्माण को सामान्य बनाने के लिए: SFINAE पर आधारित प्रायोगिक लक्षण हैं जिनका detected_or detected_t is_detected

टेम्पलेट पैरामीटर typename Default , template <typename...> Op और typename ... Args :

  • is_detected : is_detected of std::true_type या std::false_type Op<Args...> की वैधता के आधार पर Op<Args...>
  • detected_t : की उर्फ Op<Args...> या nonesuch की वैधता के आधार पर Op<Args...>
  • detected_or : के साथ एक struct की उर्फ value_t जो है is_detected , और type जो है Op<Args...> या Default की वैधता के आधार पर Op<Args...>

जिसे निम्नलिखित के रूप में SFINAE के लिए std::void_t का उपयोग करके लागू किया जा सकता है:

सी ++ 17
namespace detail {
    template <class Default, class AlwaysVoid,
              template<class...> class Op, class... Args>
    struct detector
    {
        using value_t = std::false_type;
        using type = Default;
    };

    template <class Default, template<class...> class Op, class... Args>
    struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
    {
        using value_t = std::true_type;
        using type = Op<Args...>;
    };

} // namespace detail

// special type to indicate detection failure
struct nonesuch {
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected =
    typename detail::detector<nonesuch, void, Op, Args...>::value_t;

template <template<class...> class Op, class... Args>
using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;

template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;

विधि की उपस्थिति का पता लगाने के लिए लक्षण बस लागू किया जा सकता है:

typename <typename T, typename ...Ts>
using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));

struct C1 {};

struct C2 {
    int foo(char) const;
};

template <typename T>
using has_foo_char = is_detected<foo_type, T, char>;

static_assert(!has_foo_char<C1>::value, "Unexpected");
static_assert(has_foo_char<C2>::value, "Unexpected");

static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value,
              "Unexpected");

static_assert(std::is_same<void, // Default
                           detected_or<void, foo_type, C1, char>>::value,
              "Unexpected");
static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value,
              "Unexpected");

बड़ी संख्या में विकल्पों के साथ अधिभार संकल्प

यदि आपको कई विकल्पों के बीच चयन करने की आवश्यकता है, तो enable_if<> माध्यम से सिर्फ एक को सक्षम करना काफी बोझिल हो सकता है, क्योंकि कई स्थितियों को भी enable_if<> की आवश्यकता होती है।

इसके बजाय अधिभार के बीच क्रम को वंशानुक्रम का उपयोग करके चुना जा सकता है, अर्थात टैग प्रेषण।

उस चीज़ के लिए परीक्षण करने के बजाय जिसे अच्छी तरह से बनने की आवश्यकता है, और अन्य सभी संस्करणों की स्थिति की decltype का भी परीक्षण करते हैं, हम इसके बजाय केवल उसी चीज़ के लिए परीक्षण करते हैं decltype हमें ज़रूरत है, अधिमानतः एक अनुगामी रिटर्न में एक decltype में।
यह कई विकल्पों को अच्छी तरह से छोड़ सकता है, हम 'टैग' का उपयोग करने वालों के बीच अंतर करते हैं, जैसे कि random_access_tag -ट्रेट टैग ( random_access_tag et al)। यह काम करता है क्योंकि एक सीधा मैच बेहतर होता है एक बेस क्लास, जो बेहतर होता है कि एक बेस क्लास का बेस क्लास इत्यादि।

#include <algorithm>
#include <iterator>

namespace detail
{
    // this gives us infinite types, that inherit from each other
    template<std::size_t N>
    struct pick : pick<N-1> {};
    template<>
    struct pick<0> {};

    // the overload we want to be preferred have a higher N in pick<N>
    // this is the first helper template function
    template<typename T>
    auto stable_sort(T& t, pick<2>)
        -> decltype( t.stable_sort(), void() )
    {
        // if the container have a member stable_sort, use that
        t.stable_sort();
    }

    // this helper will be second best match
    template<typename T>
    auto stable_sort(T& t, pick<1>)
        -> decltype( t.sort(), void() )
    {
        // if the container have a member sort, but no member stable_sort
        // it's customary that the sort member is stable
        t.sort();
    }

    // this helper will be picked last
    template<typename T>
    auto stable_sort(T& t, pick<0>)
        -> decltype( std::stable_sort(std::begin(t), std::end(t)), void() )
    {
        // the container have neither a member sort, nor member stable_sort
        std::stable_sort(std::begin(t), std::end(t));
    }

}

// this is the function the user calls. it will dispatch the call
// to the correct implementation with the help of 'tags'.
template<typename T>
void stable_sort(T& t)
{
    // use an N that is higher that any used above.
    // this will pick the highest overload that is well formed.
    detail::stable_sort(t, detail::pick<10>{});
}

ओवरलोड के बीच अंतर करने के लिए आमतौर पर अन्य तरीकों का उपयोग किया जाता है, जैसे सटीक मिलान रूपांतरण से बेहतर होना, दीर्घवृत्त से बेहतर होना।

हालांकि, टैग-प्रेषण किसी भी संख्या में विकल्पों का विस्तार कर सकता है, और इरादे में थोड़ा अधिक स्पष्ट है।



Modified text is an extract of the original Stack Overflow Documentation
के तहत लाइसेंस प्राप्त है CC BY-SA 3.0
से संबद्ध नहीं है Stack Overflow