अद्वितीय अनाम प्रकारों वाली भाषा क्यों डिज़ाइन करें?


91

यह एक ऐसी चीज है जो मुझे हमेशा C ++ लैंबडा एक्सप्रेशन की एक विशेषता के रूप में गलत बताती रही है: C ++ लैंबडा एक्सप्रेशन का प्रकार अद्वितीय और गुमनाम है, मैं इसे बस नहीं लिख सकता। यहां तक ​​कि अगर मैं दो लैम्ब्डा बनाता हूं, जो कि वाक्यात्मक रूप से समान हैं, तो परिणामी प्रकार अलग-अलग हैं। परिणाम यह है कि, ए) लंबोदर को केवल उन टेम्पलेट फ़ंक्शंस में पास किया जा सकता है, जो संकलन समय, अनिर्दिष्ट प्रकार को ऑब्जेक्ट के साथ पारित करने की अनुमति देते हैं, और बी) कि लैम्बदास केवल एक बार उपयोगी होते हैं जब वे टाइप होते हैं std::function<>

ठीक है, लेकिन यह सिर्फ जिस तरह से सी ++ करता है, मैं इसे उस भाषा की एक अपूरणीय विशेषता के रूप में लिखने के लिए तैयार था। हालाँकि, मैंने सिर्फ यह सीखा कि रस्ट प्रतीत होता है: प्रत्येक रस्ट फ़ंक्शन या लैम्ब्डा में एक अद्वितीय, अनाम प्रकार होता है। और अब मैं सोच रहा हूँ: क्यों?

तो, मेरा सवाल यह है:
भाषा डिजाइनर के दृष्टिकोण से, भाषा में अद्वितीय, अनाम प्रकार की अवधारणा को पेश करने से क्या फायदा है?


6
हमेशा की तरह बेहतर सवाल यह है कि क्यों नहीं।
स्टारगेटर

31
"यह कि लंबोदर केवल तभी उपयोगी होते हैं, जब वे std :: function <> के माध्यम से मिटाए जाते हैं।" - नहीं, वे सीधे बिना उपयोगी हैं std::function। एक लैम्ब्डा जिसे एक टेम्पलेट फ़ंक्शन में पारित किया गया है, बिना शामिल किए सीधे बुलाया जा सकता है std::function। कंपाइलर फिर लैम्ब्डा को टेम्प्लेट फ़ंक्शन में इनलाइन कर सकता है जो रनटाइम दक्षता में सुधार करेगा।
एर्लोकेंग जूल

1
मेरा अनुमान है, यह लैम्बडा को आसान बनाता है, और भाषा को समझने में आसान बनाता है। यदि आपने एक ही प्रकार के लैम्ब्डा अभिव्यक्ति को एक ही प्रकार में मोड़ने की अनुमति दी है, तो आपको विशेष नियमों की आवश्यकता होगी { int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }क्योंकि यह जिस चर का जिक्र कर रहा है वह भिन्न है, भले ही वे एक ही हों। यदि आप कहते हैं कि वे सभी अद्वितीय हैं, तो आपको यह पता लगाने की कोशिश करने की चिंता नहीं है।
नाथनऑलिवर

5
लेकिन आप एक मेमने को एक प्रकार का नाम दे सकते हैं और उसके साथ भी ऐसा ही कर सकते हैं। lambdas_type = decltype( my_lambda);
बड़ा_प्रतिष्ठ___३०३५__१

3
लेकिन जेनेरिक लैंबडा का एक प्रकार क्या होना चाहिए [](auto) {}? इसके साथ शुरू करने के लिए एक प्रकार होना चाहिए?
Evg

जवाबों:


77

कई मानक (विशेष रूप से C ++) यह न्यूनतम करने का दृष्टिकोण लेते हैं कि वे संकलक से कितनी मांग करते हैं। सच कहूँ तो, वे पहले से ही पर्याप्त मांग करते हैं! यदि उन्हें इसे काम करने के लिए कुछ निर्दिष्ट करने की आवश्यकता नहीं है, तो उनके पास इसे लागू करने को छोड़ने की प्रवृत्ति है।

अनाम होने के लिए लंबोदर थे, हमें उन्हें परिभाषित करना होगा। यह एक बड़ी बात कहना होगा कि चर कैसे पकड़े जाते हैं। एक लंबोदर के मामले पर विचार करें [=](){...}। प्रकार को यह निर्दिष्ट करना होगा कि कौन से प्रकार वास्तव में लंबोदर द्वारा कब्जा कर लिया गया था, जो निर्धारित करने के लिए गैर-तुच्छ हो सकता है। इसके अलावा, क्या होगा अगर कंपाइलर किसी वैरिएबल को सफलतापूर्वक ऑप्टिमाइज़ करता है? विचार करें:

static const int i = 5;
auto f = [i]() { return i; }

एक अनुकूलन करने वाला कंपाइलर आसानी से पहचान iसकता है कि उस पर कब्जा करने का एकमात्र संभव मूल्य 5 है, और इसके साथ प्रतिस्थापित करें auto f = []() { return 5; }। हालाँकि, यदि प्रकार अनाम नहीं है, तो यह प्रकार को बदल सकता है या संकलक को कम अनुकूलन करने के लिए मजबूर कर सकता है , iभले ही वास्तव में इसकी आवश्यकता न हो। यह जटिलता और बारीकियों का एक पूरा थैला है जो केवल उस काम के लिए आवश्यक नहीं है जो लैम्ब्डा करने का इरादा था।

और, ऑफ-केस पर कि आपको वास्तव में एक गैर-अनाम प्रकार की आवश्यकता है, आप हमेशा क्लोजर क्लास का निर्माण कर सकते हैं, और एक लंबर फ़ंक्शन के बजाय एक फ़ंक्टर के साथ काम कर सकते हैं। इस प्रकार, वे लैम्बदास को 99% मामले को संभाल सकते हैं, और आपको 1% में अपना समाधान कोड करने के लिए छोड़ सकते हैं।


Deduplicator ने टिप्पणियों में बताया कि मैंने विशिष्टता को गुमनामी के रूप में संबोधित नहीं किया था। मैं विशिष्टता के लाभों के बारे में कम निश्चित हूं, लेकिन यह ध्यान देने योग्य है कि निम्नलिखित का व्यवहार स्पष्ट है यदि प्रकार अद्वितीय हैं (कार्रवाई दो बार त्वरित होगी)।

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

यदि प्रकार अद्वितीय नहीं थे, तो हमें यह निर्दिष्ट करना होगा कि इस मामले में क्या व्यवहार होना चाहिए। यह मुश्किल हो सकता है। गुमनामी के विषय पर उठाए गए कुछ मुद्दे इस मामले में अद्वितीयता के लिए अपना बदसूरत सिर भी उठाते हैं।


ध्यान दें कि यह वास्तव में एक संकलक के लिए काम को बचाने के बारे में नहीं है, लेकिन मानकों के रखरखाव के लिए बचत कार्य है। कंपाइलर को अभी भी इसके विशिष्ट कार्यान्वयन के लिए उपरोक्त सभी प्रश्नों का उत्तर देना है, लेकिन वे मानक में निर्दिष्ट नहीं हैं।
ComicSansMS

2
@ComicSansMS एक कंपाइलर को लागू करते समय ऐसी चीजों को एक साथ रखना बहुत आसान है जब आपको अपने कार्यान्वयन को किसी और के मानक पर फिट नहीं करना पड़ता है। अनुभव से बात करें, तो अक्सर कार्यक्षमता बनाए रखने के लिए मानकों के अनुरक्षक पर यह कहीं अधिक आसान होता है, क्योंकि यह आपकी भाषा से वांछित कार्यक्षमता प्राप्त करते समय निर्दिष्ट करने के लिए न्यूनतम राशि खोजने का प्रयास करना है। एक उत्कृष्ट मामले के अध्ययन के रूप में, अभी भी देखें कि उन्होंने कितने काम खर्च किए हैं, जबकि वह अभी भी इसे उपयोगी बना रहा है (कुछ आर्किटेक्चर पर)
Cort Ammon

1
बाकी सभी की तरह, आप अनाम के लिए एक सम्मोहक मामला बनाते हैं । लेकिन क्या यह वास्तव में ऐसा एक अच्छा विचार है जो इसे अद्वितीय होने के लिए मजबूर करता है ?
Deduplicator

यह संकलक की जटिलता नहीं है जो यहां मायने रखता है, बल्कि उत्पन्न कोड की जटिलता है। बिंदु संकलक को सरल बनाने के लिए नहीं है, बल्कि सभी मामलों को अनुकूलित करने और लक्ष्य प्लेटफ़ॉर्म के लिए प्राकृतिक कोड का उत्पादन करने के लिए इसे पर्याप्त wiggle कमरा देने के लिए है।
Jan Hudec

आप एक स्थिर चर पर कब्जा नहीं कर सकते।
रुस्लान

69

लैम्ब्डा केवल फ़ंक्शन नहीं हैं, वे एक फ़ंक्शन और एक राज्य हैं । इसलिए C ++ और Rust दोनों उन्हें कॉल ऑपरेटर ( operator()C ++ में, Fn*Rust में 3 लक्षण) के साथ एक ऑब्जेक्ट के रूप में कार्यान्वित करते हैं ।

मूल रूप से, [a] { return a + 1; }C ++ में कुछ ऐसा होता है

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

तब एक उदाहरण का उपयोग करके __SomeNameजहां लंबोदर का उपयोग किया जाता है।

जबकि रस्ट में, रस्ट || a + 1में ऐसा कुछ होगा के लिए उतर जाएगा

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

इसका मतलब यह है कि ज्यादातर लंबोदर के पास अलग-अलग प्रकार के होने चाहिए

अब, कुछ तरीके हैं जिनसे हम कर सकते हैं:

  • अनाम प्रकारों के साथ, जो दोनों भाषाओं को लागू करता है। इसका एक और परिणाम यह है कि सभी लंबोदरों का एक अलग प्रकार होना चाहिए । लेकिन भाषा डिजाइनरों के लिए, इसका एक स्पष्ट लाभ है: लैम्बदास को भाषा के अन्य पहले से मौजूद सरल भागों का उपयोग करके आसानी से वर्णित किया जा सकता है। वे भाषा के पहले से मौजूद बिट्स के आस-पास सिंटेक्स चीनी हैं।
  • लैंबडा प्रकारों के नामकरण के लिए कुछ विशेष सिंटैक्स के साथ: यह हालांकि आवश्यक नहीं है क्योंकि लैम्ब्डा का उपयोग पहले से ही C ++ में या जेनरिक और Fn*रस्ट के लक्षणों के साथ टेम्प्लेट के साथ किया जा सकता है । न तो भाषा कभी भी उन्हें ( std::functionसी ++ में या Box<Fn*>जंग में) का उपयोग करने के लिए लैम्ब्डा टाइप करने के लिए मजबूर करती है ।

यह भी ध्यान दें कि दोनों भाषाएँ इस बात पर सहमत हैं कि संदर्भ को कैप्चर न करने वाले तुच्छ लैंबडा को फ़ंक्शन पॉइंटर्स में परिवर्तित किया जा सकता है।


सरल सुविधा का उपयोग करके भाषाओं की जटिल विशेषताओं का वर्णन करना बहुत आम है। उदाहरण के लिए C ++ और Rust दोनों में रेंज-फॉर लूप्स हैं, और वे दोनों उन्हें अन्य विशेषताओं के लिए सिंटैक्स चीनी के रूप में वर्णित करते हैं।

C ++ परिभाषित करता है

for (auto&& [first,second] : mymap) {
    // use first and second
}

के बराबर होने के नाते

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

और जंग परिभाषित करता है

for <pat> in <head> { <body> }

के बराबर होने के नाते

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

जबकि वे एक मानव के लिए अधिक जटिल लगते हैं, दोनों भाषा डिजाइनर या संकलक के लिए सरल होते हैं।


15
@ cmaster-reinstatemonica एक छँटाई समारोह के लिए एक तुलनित्र तर्क के रूप में एक लंबो पास करने पर विचार करें। क्या आप वास्तव में यहां वर्चुअल फ़ंक्शन कॉल का ओवरहेड लगाना चाहेंगे?
डैनियल लैंगर जूल

5
@ cmaster-reinstatemonica क्योंकि C ++ में कुछ भी वर्चुअल-बाय-डिफॉल्ट नहीं है
Caleth

4
@cmaster - इसका मतलब है कि लैम्ब्डा के सभी उपयोगकर्ताओं को डायनेमिक डिपैच के लिए भुगतान करना होगा, तब भी जब उन्हें इसकी आवश्यकता नहीं होगी?
स्टोरीटेलर - अनसलैंडर मोनिका

4
@ cmaster-reinstatemonica आपको जो सबसे अच्छा मिलेगा वह वर्चुअल में ऑप्ट-इन है। लगता है कि, क्या std::functionकरता है
Caleth

9
@ cmaster-reinstatemonica कोई भी तंत्र जहाँ आप कहे जाने वाले फ़ंक्शन को पुनः इंगित कर सकते हैं, रनटाइम ओवरहेड के साथ स्थितियाँ होंगी। यह C ++ तरीका नहीं है। आप में चुनते हैंstd::function
Caleth

13

(कैलथ के जवाब में जोड़ना, लेकिन एक टिप्पणी में फिट होने के लिए बहुत लंबा।)

लैम्ब्डा अभिव्यक्ति एक अनाम संरचना (एक वोल्डेमॉर्ट प्रकार, क्योंकि आप इसका नाम नहीं कह सकते हैं) के लिए सिंटैक्टिक शुगर है।

आप इस कोड स्निपेट में एक अनाम संरचना और एक लैम्ब्डा की गुमनामी के बीच समानता देख सकते हैं:

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

यदि वह अभी भी एक लंबोदर के लिए असंतोषजनक है, तो उसे एक गुमनाम संरचना के लिए असंतोषजनक होना चाहिए।

कुछ भाषाएँ एक प्रकार की बत्तख टाइपिंग की अनुमति देती हैं, जो थोड़ी अधिक लचीली होती है, और भले ही C ++ में ऐसे टेम्प्लेट होते हैं जो वास्तव में एक टेम्प्लेट से एक ऑब्जेक्ट बनाने में मदद नहीं करते हैं जिसमें एक सदस्य फ़ील्ड है जो लैम्बडा को बदलने के बजाय सीधे उपयोग कर सकता है std::functionआवरण।


3
धन्यवाद, कि वास्तव में सी लाम्डास को सी ++ में परिभाषित करने के पीछे तर्क पर थोड़ा प्रकाश डाला गया है (मुझे "वोल्डेमॉर्ट टाइप" :-) शब्द याद था)। हालांकि, सवाल यह है कि भाषा डिजाइनर की नजर में इसका क्या फायदा है?
सेंटास्टर -

1
आप int& operator()(){ return x; }उन
संरचनाओं

2
@ cmaster-reinstatemonica • विशिष्ट रूप से ... शेष C ++ इस तरह से व्यवहार करता है। लैम्ब्डा बनाने के लिए "सतह के आकार" के कुछ प्रकार का उपयोग करते हुए बतख टाइपिंग बाकी भाषा से बहुत अलग होगी। लैम्ब्डा के लिए भाषा में उस तरह की सुविधा को शामिल करना संभवतः पूरी भाषा के लिए सामान्यीकृत माना जाएगा, और यह संभावित रूप से बहुत बड़ा परिवर्तन होगा। सिर्फ लंबोदर के लिए इस तरह की सुविधा को स्वीकार करना बाकी सी ++ की दृढ़ता से-ईश टाइपिंग के साथ फिट बैठता है।
एलजय जूल

तकनीकी रूप से एक वोल्डेमॉर्ट प्रकार होगा auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; }, क्योंकि बाहर fooकुछ भी पहचानकर्ता का उपयोग नहीं कर सकता हैDarkLord
Caleth

@ cmaster-reinstatemonica दक्षता, विकल्प बॉक्स के लिए होगा और गतिशील रूप से प्रत्येक लैम्ब्डा (इसे ढेर पर आवंटित करें और इसके सटीक प्रकार को मिटा दें)। अब आप ध्यान दें के रूप में संकलक सकता lambdas की अनाम प्रकार deduplicate, लेकिन आप अभी भी उन्हें लिख करने में सक्षम नहीं होगा और यह बहुत कम लाभ के लिए महत्वपूर्ण कार्य की आवश्यकता होगी तो बाधाओं पक्ष में वास्तव में नहीं हैं।
14-28 पर मास्किनिन

10

अद्वितीय अनाम प्रकारों वाली भाषा क्यों डिज़ाइन करें ?

क्योंकि ऐसे मामले हैं जहां नाम अप्रासंगिक हैं और उपयोगी या काउंटर-उत्पादक नहीं हैं। इस स्थिति में उनके अस्तित्व का सार उपयोगी है क्योंकि यह नाम प्रदूषण को कम करता है, और कंप्यूटर विज्ञान में दो कठिन समस्याओं में से एक को हल करता है (चीजों को कैसे नाम दें)। उसी कारण से, अस्थायी वस्तुएं उपयोगी हैं।

लैम्ब्डा

विशिष्टता कोई विशेष मेमने की चीज नहीं है, या यहां तक ​​कि विशेष प्रकार की गुमनाम चीजों की भी नहीं है। यह भाषा में नामित प्रकारों पर भी लागू होता है। निम्नलिखित पर विचार करें:

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

ध्यान दें कि मैं पारित नहीं हो सकता Bमें foo, भले ही कक्षाएं समान हैं। यह समान गुण अनाम प्रकारों पर लागू होता है।

lambdas को केवल उन टेम्पलेट फ़ंक्शंस में पास किया जा सकता है जो संकलन समय, अनिर्दिष्ट प्रकार को ऑब्जेक्ट के साथ पास करने की अनुमति देता है ... std के माध्यम से मिटाया गया :: फ़ंक्शन <>।

लैम्ब्डा के सबसेट के लिए तीसरा विकल्प है: नॉन-कैप्चरिंग लैम्ब्डा को फंक्शन पॉइंटर्स में बदला जा सकता है।


ध्यान दें कि यदि अनाम प्रकार की सीमाएं उपयोग के मामले के लिए एक समस्या हैं, तो समाधान सरल है: इसके बजाय एक नामित प्रकार का उपयोग किया जा सकता है। लैम्ब्डा ऐसा कुछ भी नहीं करता है जिसे एक नामित वर्ग के साथ नहीं किया जा सकता है।


10

Cort Ammon का स्वीकृत उत्तर अच्छा है, लेकिन मुझे लगता है कि कार्यान्वयन के बारे में एक और महत्वपूर्ण बिंदु है।

मान लीजिए कि मेरी दो अलग-अलग अनुवाद इकाइयाँ हैं, "one.cpp" और "two.cpp"।

// one.cpp
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);

extern void foo(A1);
extern void foo(B1);

fooएक ही पहचानकर्ता ( foo) का उपयोग करने के दो अधिभार, लेकिन अलग-अलग नाम हैं। (POSIX-ish सिस्टम पर उपयोग किए जाने वाले इटेनियम एबी _Z3foo1Aमें, इस विशेष मामले में, गिरे हुए नाम हैं और _Z3fooN1bMUliE_E।)

// two.cpp
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);

void foo(A2) {}
void foo(B2) {}

सी ++ संकलक को यह सुनिश्चित करना चाहिए कि void foo(A1)"दो.cpp" में मंगला नाम "one.cpp" के मंगली नाम के समान हो extern void foo(A2), ताकि हम दो ऑब्जेक्ट फ़ाइलों को एक साथ लिंक कर सकें। यह दो प्रकार का भौतिक अर्थ है "एक ही प्रकार": यह अनिवार्य रूप से अलग-अलग संकलित ऑब्जेक्ट फ़ाइलों के बीच एबीआई-संगतता के बारे में है।

यह सुनिश्चित करने के लिए सी ++ संकलक की आवश्यकता नहीं है B1और B2"समान प्रकार" हैं। (वास्तव में, यह सुनिश्चित करना आवश्यक है कि वे अलग-अलग प्रकार के हों; लेकिन यह अभी उतना महत्वपूर्ण नहीं है।)


यह सुनिश्चित करने के लिए संकलक किस भौतिक तंत्र का उपयोग करता है A1और A2"समान प्रकार" हैं?

यह बस टाइपसेफ के माध्यम से दफन करता है, और फिर प्रकार के पूरी तरह से योग्य नाम को देखता है। यह एक वर्ग प्रकार का नाम है A। (कुंआ,::A चूंकि यह वैश्विक नामस्थान में है।) इसलिए यह दोनों मामलों में एक ही प्रकार का है। यह समझना आसान है। इससे भी महत्वपूर्ण बात, इसे लागू करना आसान है । यह देखने के लिए कि क्या दो प्रकार के वर्ग समान हैं, आप उनके नाम लेते हैं और एक करते हैं strcmp। किसी वर्ग प्रकार को किसी फ़ंक्शन के मांगे गए नाम से जोड़ने के लिए, आप उसके नाम में वर्णों की संख्या लिखते हैं, उसके बाद उन वर्णों को लिखते हैं।

तो, नाम प्रकार आसान करने के लिए आसान कर रहे हैं।

यह सुनिश्चित करने के लिए कंपाइलर किस भौतिक तंत्र का उपयोग कर सकता हैB1B2 हैं और "उसी प्रकार के" हैं, काल्पनिक दुनिया में जहां C ++ के लिए उन्हें उसी प्रकार का होना आवश्यक था?

खैर, यह प्रकार के नाम का उपयोग नहीं कर सका, क्योंकि प्रकार नहीं करता है में नाम है

हो सकता है कि यह किसी तरह लंबोदर के शरीर के पाठ को एनकोड कर सके । लेकिन यह एक तरह से अजीब होगा, क्योंकि वास्तव bमें "one.cpp" "दो.cpp" से बिल्कुल अलग है b: "one.cpp" में x+1और "two.cpp" है x + 1। इसलिए हमें एक नियम के साथ आना होगा जो या तो कहता है कि यह व्हाट्सएप अंतर कोई फर्क नहीं पड़ता है, या यह है कि यह (उन्हें विभिन्न प्रकार के सभी बनाने के बाद), या हो सकता है कि यह (शायद कार्यक्रम की वैधता कार्यान्वयन-परिभाषित है) , या हो सकता है कि यह "बीमार-गठित कोई नैदानिक ​​आवश्यक नहीं है")। वैसे भी,A

कठिनाई से सबसे आसान तरीका बस यह कहना है कि प्रत्येक लैम्ब्डा अभिव्यक्ति एक अद्वितीय प्रकार के मूल्यों का उत्पादन करती है। फिर विभिन्न अनुवाद इकाइयों में परिभाषित दो लंबोदर प्रकार निश्चित रूप से एक ही प्रकार के नहीं होते हैं । एक एकल अनुवाद इकाई के भीतर, हम स्रोत कोड की शुरुआत से गिनती करके "नाम" लंबोदर प्रकार कर सकते हैं:

auto a = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

बेशक इन नामों का अर्थ केवल इस अनुवाद इकाई के भीतर है। यह TU's $_0हमेशा कुछ अन्य TU से अलग प्रकार का होता है $_0, भले ही यह TU struct Aहमेशा की तरह कुछ अन्य TU का है struct A

वैसे, ध्यान दें कि हमारे "लंबो के पाठ को सांकेतिक शब्दों में बदलना" विचार की एक और सूक्ष्म समस्या थी: लंबोदा $_2और $_3जिसमें एक ही पाठ शामिल है , लेकिन उन्हें स्पष्ट रूप से एक ही प्रकार नहीं माना जाना चाहिए !


वैसे, सी ++ पता करने के लिए एक मनमाना सी ++ के पाठ वध करने के लिए कैसे संकलक की आवश्यकता होती है अभिव्यक्ति , के रूप में

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

लेकिन C ++ को (अभी तक) संकलक को यह जानने की आवश्यकता नहीं है कि मनमाने ढंग से C ++ स्टेटमेंट को कैसे मेनटेन किया जाएdecltype([](){ ...arbitrary statements... })अभी भी C ++ 20 में बीमार है।


यह भी ध्यान दें कि किसी अनजान प्रकार का उपयोग करके स्थानीय उपनाम देना आसान है typedef/ using। मुझे लग रहा है कि आपका सवाल कुछ ऐसा करने की कोशिश करने से पैदा हो सकता है जो इस तरह से हल किया जा सकता है।

auto f(int x) {
    return [x](int y) { return x+y; };
}

// Give the type an alias, so I can refer to it within this translation unit
using AdderLambda = decltype(f(0));

int of_one(AdderLambda g) { return g(1); }

int main() {
    auto f1 = f(1);
    assert(of_one(f1) == 2);
    auto f42 = f(42);
    assert(of_one(f42) == 43);
}

EDD TO ADD: अन्य उत्तरों पर आपकी कुछ टिप्पणियों को पढ़ने से, ऐसा लगता है कि आप सोच रहे हैं कि क्यों

int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);

ऐसा इसलिए है क्योंकि कैप्चरलेस लैम्ब्डा डिफ़ॉल्ट-कंस्ट्रक्टेबल हैं। (C ++ में केवल C ++ 20 के रूप में, लेकिन यह हमेशा वैचारिक रूप से सत्य रहा है।)

template<class T>
int default_construct_and_call(int x) {
    T t;
    return t(x);
}

assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);

यदि आपने कोशिश की है default_construct_and_call<decltype(&add1)>, tतो एक डिफ़ॉल्ट-आरंभिक फ़ंक्शन पॉइंटर होगा और आप शायद सेगफ़ॉल्ट होंगे। वह, जैसे, उपयोगी नहीं है।


" वास्तव में, यह सुनिश्चित करने की आवश्यकता है कि वे अलग-अलग प्रकार के हों; लेकिन यह अभी उतना महत्वपूर्ण नहीं है। " मुझे आश्चर्य है कि अगर समान रूप से परिभाषित होने पर विशिष्टता को मजबूर करने का एक अच्छा कारण है।
Deduplicator

व्यक्तिगत रूप से मुझे लगता है कि पूरी तरह से परिभाषित व्यवहार (लगभग?) हमेशा अनिर्दिष्ट व्यवहार से बेहतर है। "क्या ये दो फंक्शन पॉइंट बराबर हैं? ठीक है, केवल अगर ये दो टेम्प्लेट इंस्टेंटिएशन एक ही फंक्शन हैं, जो कि केवल दो लैम्ब्डा टाइप एक ही प्रकार के हैं, जो कि कंपाइलर ने उन्हें मर्ज करने का फैसला किया है, तो ही सही है।" को भावुक! (लेकिन ध्यान दें कि हमारे पास स्ट्रिंग-शाब्दिक विलय के साथ एक बिल्कुल अनुरूप स्थिति है, और कोई भी उस स्थिति के बारे में हैरान नहीं है । इसलिए मुझे संदेह है कि संकलक को समान प्रकार के विलय की अनुमति देना भयावह होगा।)
क्वक्सप्लस

ठीक है, क्या दो समान कार्य (जैसे-अगर) समान हो सकते हैं, यह भी एक अच्छा सवाल है। मानक में भाषा मुक्त और / या स्थिर कार्यों के लिए बिल्कुल स्पष्ट नहीं है। लेकिन वह यहां के दायरे से बाहर है।
Deduplicator

गंभीर रूप से, एलएलवीएम मेलिंग सूची पर इसी महीने विलय के कार्यों के बारे में चर्चा हुई है । क्लैंग कोडेन पूरी तरह से खाली निकायों के साथ फ़ंक्शंस का कारण बनेंगे, जो लगभग "दुर्घटना से" विलीन हो जाएंगे : godbolt.org/z/obT55b यह तकनीकी रूप से गैर-अनुरूप है, और मुझे लगता है कि वे ऐसा करने से रोकने के लिए संभावित एलएलवीएम पैच करेंगे। लेकिन हां, सहमति, समारोह के पते को मर्ज करना भी एक बात है।
क्क्सप्लसोन

उस उदाहरण में अन्य समस्याएं हैं, अर्थात् लापता विवरण-कथन। क्या वे अकेले पहले से ही कोड को गैर-अनुरूप नहीं बनाते हैं? इसके अलावा, मैं चर्चा के लिए देखूंगा, लेकिन क्या वे दिखाते हैं या समतुल्य समतुल्य कार्यों को मिलाते हैं, मानक, उनके प्रलेखित व्यवहार के लिए गैर-अनुरूप है, जीसीसी के लिए, या बस कुछ इस पर भरोसा करते हैं कि ऐसा नहीं हो रहा है?
Deduplicator

9

C ++ लैम्ब्डा को अलग-अलग ऑपरेशन के लिए अलग-अलग प्रकार की आवश्यकता होती है , क्योंकि C ++ स्टेटिकली बांधता है। वे केवल प्रतिलिपि / चाल-निर्माण योग्य हैं, इसलिए ज्यादातर आपको उनके प्रकार का नाम देने की आवश्यकता नहीं है। लेकिन यह सब कुछ एक कार्यान्वयन विवरण का है।

मुझे यकीन नहीं है कि अगर सी # लैंबडास का एक प्रकार है, क्योंकि वे "अनाम फ़ंक्शन एक्सप्रेशन" हैं, और वे तुरंत एक संगत प्रतिनिधि प्रकार या अभिव्यक्ति ट्री प्रकार में परिवर्तित हो जाते हैं। यदि ऐसा है, तो यह संभवतः एक अप्राप्य प्रकार है।

C ++ में अनाम संरचनाएं भी हैं, जहां प्रत्येक परिभाषा एक अद्वितीय प्रकार की ओर ले जाती है। यहां नाम अप्रमाणिक नहीं है, यह केवल मानक के संबंध में मौजूद नहीं है।

C # में अज्ञात डेटा प्रकार होते हैं , जो इसे ध्यान से परिभाषित किए गए दायरे से भागने से मना करते हैं। कार्यान्वयन उन लोगों को भी एक अद्वितीय, अप्राप्य नाम देता है।

प्रोग्रामर को एक अनाम प्रकार के संकेत होने के बाद कि वे अपने कार्यान्वयन के अंदर चारों ओर प्रहार न करें।

एक तरफ:

आप एक मेमने के प्रकार को एक नाम दे सकते हैं।

auto foo = []{}; 
using Foo_t = decltype(foo);

यदि आपके पास कोई कब्जा नहीं है, तो आप एक फ़ंक्शन पॉइंटर प्रकार का उपयोग कर सकते हैं

void (*pfoo)() = foo;

1
पहला उदाहरण कोड अभी भी बाद में Foo_t = []{};, केवल Foo_t = fooऔर कुछ नहीं की अनुमति देगा ।
विस्फ़ोटक -

1
@ cmaster-reinstatemonica ऐसा इसलिए है क्योंकि प्रकार डिफ़ॉल्ट रूप से डिफ़ॉल्ट नहीं है, गुमनामी के कारण नहीं। मेरा अनुमान है कि किसी भी तकनीकी कारण के रूप में आपको याद रखने वाले कोने के मामलों का एक बड़ा सेट होने से बचने के लिए उतना ही करना है।
कैलथ जूल

6

अनाम प्रकारों का उपयोग क्यों करें?

कंपाइलर द्वारा स्वचालित रूप से उत्पन्न होने वाले प्रकारों के लिए, विकल्प या तो (1) प्रकार के नाम के लिए उपयोगकर्ता के अनुरोध का सम्मान करना है, या (2) कंपाइलर को अपने दम पर चुनने दें।

  1. पूर्व मामले में, उपयोगकर्ता से स्पष्ट रूप से एक नाम प्रदान करने की अपेक्षा की जाती है जब हर बार ऐसा निर्माण दिखाई देता है (C ++ / Rust: जब भी एक लैम्ब्डा को परिभाषित किया जाता है; जंग: जब भी कोई फ़ंक्शन परिभाषित होता है)। यह उपयोगकर्ता को हर बार प्रदान करने के लिए एक थकाऊ विवरण है, और अधिकांश मामलों में नाम को फिर से संदर्भित नहीं किया जाता है। इस प्रकार यह समझ में आता है कि कंपाइलर अपने लिए एक नाम का पता लगाने देता है, और मौजूदा सुविधाओं जैसे कि decltypeया टाइप इंफ़ेक्शन का उपयोग उन कुछ स्थानों पर टाइप करने के लिए करता है जहाँ इसकी आवश्यकता है।

  2. बाद के मामले में, संकलक को प्रकार के लिए एक अद्वितीय नाम चुनने की आवश्यकता होती है, जो संभवतः एक अस्पष्ट, अपठनीय नाम होगा __namespace1_module1_func1_AnonymousFunction042। भाषा डिजाइनर यह निर्दिष्ट कर सकता है कि यह नाम कैसे शानदार और नाजुक विवरण में निर्मित किया गया है, लेकिन यह अनावश्यक रूप से उपयोगकर्ता के लिए एक कार्यान्वयन विस्तार को उजागर करता है कि कोई भी समझदार उपयोगकर्ता इस पर भरोसा नहीं कर सकता है, क्योंकि नाम भी मामूली रिफ्लेक्टर के चेहरे में भंगुर नहीं है। यह अनावश्यक रूप से भाषा के विकास को भी बाधित करता है: भविष्य की सुविधा के कारण मौजूदा नाम पीढ़ी एल्गोरिथ्म को बदलने का कारण हो सकता है, जिससे पिछड़े संगतता मुद्दे पैदा हो सकते हैं। इस प्रकार, यह समझ में आता है कि इस विवरण को छोड़ दिया जाए, और यह सुनिश्चित किया जाए कि उपयोगकर्ता द्वारा ऑटो-जेनरेट किया गया प्रकार अव्यावहारिक है।

अद्वितीय (विशिष्ट) प्रकारों का उपयोग क्यों करें?

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

एक उदाहरण के रूप में, संकलक जिस क्षण को देखता है:

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

संकलक को पूर्ण विश्वास है कि gआवश्यक रूप से f, यहां तक ​​कि उत्पत्ति से भी जाना जाना चाहिए g। यह कॉल को gभक्तिपूर्ण बनाने की अनुमति देगा । उपयोगकर्ता को यह भी पता होगा, क्योंकि उपयोगकर्ता ने fडेटा के प्रवाह के माध्यम से अद्वितीय प्रकार को संरक्षित करने के लिए बहुत सावधानी बरती हैg

आवश्यक रूप से, यह बाधा है कि उपयोगकर्ता क्या कर सकता है f। उपयोगकर्ता लिखने के लिए स्वतंत्रता में नहीं है:

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

इससे दो अलग-अलग प्रकारों का (अवैध) एकीकरण होगा।

इसके चारों ओर काम करने के लिए, उपयोगकर्ता __UniqueFunc042गैर-अद्वितीय प्रकार के लिए अपस्टैंड कर सकता है &dyn Fn(),

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

इस प्रकार के विलोपन द्वारा किया गया व्यापार बंद है &dyn Fn(), जो कंपाइलर के लिए तर्क को जटिल बनाता है। दिया हुआ:

let g2: &dyn Fn() = /*expression */;

संकलक /*expression */को यह निर्धारित करने के लिए श्रमसाध्य रूप से जांचना पड़ता है कि क्या इससे या किसी अन्य फ़ंक्शन (ओं) g2से उत्पन्न होता है f, और यह कि किन परिस्थितियों में यह सिद्ध होता है। कई परिस्थितियों में, संकलक छोड़ देना हो सकता है: शायद मानव कि बता सकते हैं g2वास्तव में से आता है fसभी परिस्थितियों में लेकिन से पथ fको g2भी समझने के लिए संकलक के लिए घुमावदार किया गया था, के लिए एक आभासी गए कॉल मेंg2 निराशावादी प्रदर्शन के साथ।

यह तब और अधिक स्पष्ट हो जाता है जब ऐसी वस्तुओं को जेनेरिक (टेम्प्लेट) कार्यों में वितरित किया जाता है:

fn h<F: Fn()>(f: F);

एक कॉल करता है h(f), जहां f: __UniqueFunc042है, तो hएक अनूठा उदाहरण के लिए विशेष है:

h::<__UniqueFunc042>(f);

यह संकलक के लिए hविशेष तर्क के अनुरूप, विशेष कोड उत्पन्न करने के लिए सक्षम बनाता है f, और यदि प्रेषित fनहीं है, तो प्रेषण काफी स्थिर होने की संभावना है।

विपरीत परिस्थिति में, जहां एक के h(f)साथ कॉल किया जाता है f2: &Fn(), के hरूप में त्वरित किया जाता है

h::<&Fn()>(f);

जिसे सभी प्रकारों के बीच साझा किया जाता है &Fn()। भीतर से h, संकलक प्रकार के एक अपारदर्शी समारोह के बारे में बहुत कम जानता है &Fn()और इसलिए केवल रूढ़िवादी रूप fसे एक आभासी प्रेषण के साथ कॉल कर सकता है । संवैधानिक रूप से भेजने के लिए, कंपाइलर को h::<&Fn()>(f)अपने कॉल साइट पर कॉल को इनलाइन करना होगा , जो hकि बहुत जटिल होने पर गारंटी नहीं है।


नाम चुनने के बारे में पहला भाग इस बिंदु को याद करता है: एक प्रकार की तरह void(*)(int, double)एक नाम नहीं हो सकता है, लेकिन मैं इसे लिख सकता हूं। मैं इसे एक अनाम प्रकार नहीं, बल्कि एक अनाम प्रकार कहूंगा। और मैं __namespace1_module1_func1_AnonymousFunction042क्रिप्टोकरंसी जैसे नाम की तबाही कहूंगा , जो निश्चित रूप से इस सवाल के दायरे में नहीं है। यह प्रश्न उन प्रकारों के बारे में है जो मानक द्वारा नीचे लिखे जाने के लिए असंभव होने की गारंटी देते हैं, एक प्रकार के वाक्यविन्यास को पेश करने का विरोध करते हैं जो इन प्रकारों को एक उपयोगी तरीके से व्यक्त कर सकते हैं।
cmaster - मोनिका

3

सबसे पहले, कैप्चर के बिना लैम्ब्डा एक फ़ंक्शन पॉइंटर के लिए परिवर्तनीय है। इसलिए वे कुछ उदारता प्रदान करते हैं।

अब कब्जा करने वाले लंबोदर सूचक के लिए परिवर्तनीय क्यों नहीं हैं? क्योंकि फ़ंक्शन को लैम्ब्डा की स्थिति तक पहुंचना चाहिए, इसलिए इस राज्य को फ़ंक्शन तर्क के रूप में प्रदर्शित होने की आवश्यकता होगी।


अच्छा, कैद तो मेमने का हिस्सा बन जाना चाहिए, नहीं? जैसे वे एक के भीतर समझाया जाता है std::function<>
सेंटास्टर -

3

उपयोगकर्ता कोड के साथ नाम टकराव से बचने के लिए।

यहां तक ​​कि एक ही कार्यान्वयन वाले दो लंबोदर के अलग-अलग प्रकार होंगे। जो ठीक है क्योंकि मैं वस्तुओं के लिए अलग-अलग प्रकार का हो सकता है, भले ही उनकी मेमोरी लेआउट समान हो।


एक प्रकार जैसे int (*)(Foo*, int, double)उपयोगकर्ता कोड के साथ नाम के टकराव का कोई जोखिम नहीं है।
विस्फ़ोटक -

आपका उदाहरण बहुत अच्छी तरह से सामान्यीकृत नहीं है। हालांकि एक लैम्ब्डा अभिव्यक्ति केवल वाक्यविन्यास है यह विशेष रूप से कैप्चर क्लॉज के साथ कुछ संरचना का मूल्यांकन करेगा। इसका नामकरण स्पष्ट रूप से पहले से मौजूद संरचनाओं के नाम की गड़बड़ी का कारण बन सकता है।
नाइट जूल

फिर, यह सवाल भाषा डिजाइन के बारे में है, न कि C ++ के बारे में। मैं निश्चित रूप से एक भाषा को परिभाषित कर सकता हूं जहां एक लैम्बडा का प्रकार एक फ़ंक्शन पॉइंटर प्रकार से अधिक डेटा संरचना प्रकार के समान है। C ++ में फ़ंक्शन पॉइंटर सिंटैक्स और C में डायनेमिक एरे टाइप सिंटैक्स यह साबित करते हैं कि यह संभव है। और यह सवाल उठता है, कि लंबोदर ने एक समान दृष्टिकोण का उपयोग क्यों नहीं किया?
विस्फ़ोटक -

1
वेरिएबल करी (कैप्चरिंग) के कारण आप नहीं। इसे कार्य करने के लिए आपको फ़ंक्शन और डेटा दोनों की आवश्यकता होती है।
ब्लाइंडी

@ बेल्दी ओह, हां, मैं कर सकता हूं। मैं एक लैम्ब्डा को दो पॉइंटर्स वाली वस्तु के रूप में परिभाषित कर सकता हूं, एक कैप्चर ऑब्जेक्ट के लिए, और एक कोड के लिए। इस तरह की लैम्ब्डा वस्तु को मूल्य के आसपास से गुजरना आसान होगा। या मैं कैप्चर ऑब्जेक्ट की शुरुआत में कोड का एक स्टब के साथ ट्रिक्स खींच सकता हूं जो वास्तविक लैम्ब्डा कोड पर कूदने से पहले अपना खुद का पता लेता है। यह एक लैम्बडा पॉइंटर को सिंगल एड्रेस में बदल देगा। लेकिन यह अनावश्यक है क्योंकि पीपीसी प्लेटफॉर्म ने साबित किया है: पीपीसी पर, एक फ़ंक्शन पॉइंटर वास्तव में पॉइंटर्स की एक जोड़ी है। यही कारण है कि आप डाली नहीं कर सकते void(*)(void)करने के लिए void*और मानक C / C ++ में वापस आ गया।
विस्फ़ोटक -
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.