अस्थायी के संदर्भ में इस श्रेणी के लिए किसे दोषी ठहराया जाए?


15

पहली नज़र में निम्नलिखित कोड बल्कि हानिरहित दिखता है। एक उपयोगकर्ता bar()कुछ पुस्तकालय कार्यक्षमता के साथ बातचीत करने के लिए फ़ंक्शन का उपयोग करता है । (यह एक लंबे समय के लिए भी काम कर सकता है क्योंकि bar()एक गैर-अस्थायी मान या समान के संदर्भ में लौटा है।) हालांकि अब यह बस एक नया उदाहरण लौटा रहा है BBफिर से एक फ़ंक्शन a()होता है जो पुनरावृत्त प्रकार के ऑब्जेक्ट का संदर्भ देता है A। उपयोगकर्ता इस ऑब्जेक्ट को क्वेरी करना चाहता है जो एक segfault की ओर जाता है क्योंकि पुनरावृत्ति शुरू होने से पहले अस्थायी Bऑब्जेक्ट द्वारा लौटाया जाता bar()है।

मैं अभद्र हूं जो (पुस्तकालय या उपयोगकर्ता) इसके लिए दोषी है। सभी लाइब्रेरी प्रदान की गई कक्षाएं मुझे साफ दिखती हैं और निश्चित रूप से कुछ अलग नहीं कर रही हैं (सदस्यों के संदर्भों को वापस करना, स्टैक इंस्टेंस लौटना, ...) की तुलना में बहुत अधिक कोड हैं। उपयोगकर्ता को कुछ भी गलत नहीं लगता है, वह उस वस्तु के जीवनकाल से संबंधित कुछ भी किए बिना बस किसी वस्तु पर पुनरावृत्ति करता है।

(एक संबंधित प्रश्न यह हो सकता है: क्या किसी व्यक्ति को सामान्य नियम की स्थापना करनी चाहिए जो कोड को "रेंज-आधारित-के लिए पुनरावृति" नहीं करना चाहिए, जो लूप हैडर में एक से अधिक जंजीर कॉल द्वारा प्राप्त किया गया है क्योंकि इनमें से कोई भी कॉल वापस आ सकती है rvalue?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
जब आपको पता चला कि किसे दोषी ठहराया जाए, तो अगला कदम क्या होगा? उस पर चिल्लाते हुए / उसे?
जेन्सजी

7
नहीं, मैं क्यों करूंगा? मुझे वास्तव में यह जानने में अधिक दिलचस्पी है कि इस "कार्यक्रम" को विकसित करने की सोचा प्रक्रिया भविष्य में इस समस्या से बचने में विफल रही।
hllnll

इसका छोरों, या छोरों के लिए रेंज-आधारित के साथ कोई लेना-देना नहीं है, लेकिन उपयोगकर्ता के साथ वस्तु जीवनकाल को ठीक से नहीं समझती है।
जेम्स

साइट-टिप्पणी: यह CWG 900 है जिसे Not A Defect के रूप में बंद किया गया था। शायद मिनटों में कुछ चर्चा हो।
21

8
इसके लिए किसे दोषी ठहराया जाए? ब्रेज़न स्ट्रॉस्ट्रुप और डेनिस रिची, सबसे पहले और सबसे महत्वपूर्ण।
मेसन व्हीलर

जवाबों:


14

मुझे लगता है कि मौलिक समस्या C ++ की भाषा सुविधाओं (या इसके अभाव) का एक संयोजन है। लाइब्रेरी कोड और क्लाइंट कोड दोनों ही उचित हैं (जैसा कि इस तथ्य से स्पष्ट है कि समस्या स्पष्ट है)। यदि अस्थायी Bका जीवनकाल उपयुक्त (लूप के अंत तक) बढ़ाया गया था, तो कोई समस्या नहीं होगी।

अस्थायी जीवन को केवल लंबे समय तक बनाना, और अब नहीं, बेहद कठिन है। यहां तक ​​कि एक तदर्थ के रूप में भी नहीं "रेंज के निर्माण में शामिल सभी अस्थायी, लूप के अंत तक रहने के लिए रेंज-आधारित के लिए" साइड इफेक्ट्स के बिना होंगे। मूल्य द्वारा वस्तु B::a()के स्वतंत्र होने की सीमा को लौटाने के मामले पर विचार करें B। फिर अस्थायी Bको तुरंत त्याग दिया जा सकता है। यहां तक ​​कि अगर कोई उन मामलों की सटीक पहचान कर सकता है जहां जीवनकाल का विस्तार आवश्यक है, क्योंकि ये मामले प्रोग्रामर के लिए स्पष्ट नहीं हैं, तो प्रभाव (विनाशकों को बहुत बाद में कहा जाता है) आश्चर्यचकित होगा और शायद कीड़े का एक समान रूप से सूक्ष्म स्रोत।

इस तरह की बकवास का पता लगाने और मना करने के लिए यह अधिक वांछनीय होगा, प्रोग्रामर bar()को एक स्थानीय चर को स्पष्ट रूप से ऊंचा करने के लिए मजबूर करता है । यह C ++ 11 में संभव नहीं है, और शायद कभी भी संभव नहीं होगा क्योंकि इसके लिए एनोटेशन की आवश्यकता होती है। जंग यह करता है, जहां हस्ताक्षर .a()होगा:

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

यहां 'xजीवनकाल चर या क्षेत्र है, जो एक संसाधन उपलब्ध होने की अवधि के लिए एक प्रतीकात्मक नाम है। सच कहूँ, तो जीवन भर समझाना मुश्किल है - या हमने अभी तक सबसे अच्छी व्याख्या नहीं की है - इसलिए मैं इस उदाहरण के लिए खुद को न्यूनतम आवश्यक तक ही सीमित रखूंगा और आधिकारिक दस्तावेज में इच्छुक पाठक को संदर्भित करूंगा ।

उधार लेने वाला चेकर यह नोटिस करेगा कि bar().a()जब तक लूप चलता है, तब तक रहने की जरूरत है। जीवनकाल में एक बाधा के रूप में फंसा 'x, हम लिखते हैं 'loop <= 'x:। यह भी नोटिस करेगा कि मेथड कॉल का रिसीवर bar()अस्थायी है। दो 'x <= 'tempबिंदु एक ही जीवनकाल से जुड़े हैं, इसलिए एक और बाधा है।

ये दो अड़चनें विरोधाभासी हैं! हमें इसकी आवश्यकता है , 'loop <= 'x <= 'tempलेकिन 'temp <= 'loopजो समस्या को ठीक ठीक समझ लेता है। परस्पर विरोधी आवश्यकताओं के कारण, छोटी गाड़ी कोड अस्वीकार कर दिया गया है। ध्यान दें कि यह एक संकलन-समय की जांच है और जंग कोड आमतौर पर समान C ++ कोड के समान मशीन कोड के परिणामस्वरूप होता है, इसलिए आपको इसके लिए रन-टाइम लागत का भुगतान करने की आवश्यकता नहीं है।

फिर भी यह एक भाषा में जोड़ने के लिए एक बड़ी विशेषता है, और केवल तभी काम करता है जब सभी कोड इसका उपयोग करते हैं। API का डिज़ाइन भी प्रभावित होता है (कुछ डिज़ाइन जो C ++ में बहुत खतरनाक होंगे वे व्यावहारिक हो जाते हैं, अन्य को जीवनकाल के साथ अच्छा खेलने के लिए नहीं बनाया जा सकता है)। काश, इसका मतलब है कि सी + + (या वास्तव में किसी भी भाषा) को पूर्वव्यापी रूप से जोड़ना व्यावहारिक नहीं है। सारांश में, गलती जड़ता पर है सफल भाषाओं में है और तथ्य यह है कि 1983 में Bjarne क्रिस्टल बॉल और दूरदर्शिता पिछले 30 वर्षों के अनुसंधान और C ++ अनुभव ;-) के पाठ को शामिल करने के लिए नहीं था;

बेशक, यह भविष्य में समस्या से बचने के लिए बिल्कुल भी सहायक नहीं है (जब तक कि आप जंग पर स्विच नहीं करते हैं और फिर कभी सी ++ का उपयोग नहीं करते हैं)। एक कई जंजीर विधि कॉल के साथ लंबे समय तक अभिव्यक्ति से बच सकता है (जो बहुत सीमित है, और सभी जीवनकाल की परेशानियों को दूर नहीं करता है)। या कोई संकलक सहायता के बिना एक अधिक अनुशासित स्वामित्व नीति अपनाने की कोशिश कर सकता है: दस्तावेज़ स्पष्ट रूप barसे मूल्य से लौटता है और उस पर परिणाम B::a()नहीं होना चाहिए Bजिस a()पर आह्वान किया गया है। जब एक लंबे समय तक रहने वाले संदर्भ के बजाय मान से लौटने के लिए एक फ़ंक्शन को बदलते हैं, तो सचेत रहें कि यह अनुबंध का एक परिवर्तन है । फिर भी त्रुटि हो सकती है, लेकिन ऐसा होने पर कारण की पहचान करने की प्रक्रिया को गति दे सकती है।


14

क्या हम C ++ सुविधाओं का उपयोग करके इस समस्या को हल कर सकते हैं?

C ++ 11 ने सदस्य फ़ंक्शन रेफ-क्वालीफायर को जोड़ा है, जो वर्ग के उदाहरण (अभिव्यक्ति) के मूल्य श्रेणी को प्रतिबंधित करने की अनुमति देता है, जिस पर सदस्य फ़ंक्शन को बुलाया जा सकता है। उदाहरण के लिए:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

beginसदस्य फ़ंक्शन को कॉल करते समय , हम जानते हैं कि सबसे अधिक संभावना है कि हमें endसदस्य फ़ंक्शन को कॉल करने की आवश्यकता होगी (या sizeसीमा के आकार को प्राप्त करने के लिए ऐसा कुछ )। इसके लिए आवश्यक है कि हम एक अंतराल पर काम करें, क्योंकि हमें इसे दो बार संबोधित करने की आवश्यकता है। इसलिए आप तर्क दे सकते हैं कि इन सदस्य कार्यों को लंबू-रेफ-योग्य होना चाहिए।

हालाँकि, यह अंतर्निहित समस्या को हल नहीं कर सकता है: अलियासिंग। beginऔर endसदस्य समारोह उर्फ वस्तु, या संसाधनों वस्तु द्वारा प्रबंधित। यदि हम बदलते हैं beginऔर endएक ही फ़ंक्शन द्वारा range, हमें एक प्रदान करना चाहिए, जिसे rvalues ​​पर बुलाया जा सकता है:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

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

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

यह ओपी के मामले में लागू होता है, और मामूली कोड की समीक्षा

struct B {
    A m_a;
    A & a() { return m_a; }
};

यह सदस्य फ़ंक्शन अभिव्यक्ति के मूल्य श्रेणी को बदलता है: B()एक प्रचलन है, लेकिन एक अंतराल B().a()है। दूसरी ओर, B().m_aएक प्रतिद्वंद्विता है। तो चलिए इसे सुसंगत बनाकर शुरू करते हैं। इसे करने के दो तरीके हैं:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

दूसरा संस्करण, जैसा कि ऊपर कहा गया है, ओपी में समस्या को ठीक करेगा।

इसके अतिरिक्त, हम Bसदस्य के कार्यों को प्रतिबंधित कर सकते हैं :

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

ओपी के कोड पर इसका कोई प्रभाव नहीं पड़ेगा, क्योंकि :लूप के लिए रेंज-आधारित में अभिव्यक्ति का परिणाम एक संदर्भ चर के लिए बाध्य है। और यह चर (इसके beginऔर endसदस्य कार्यों तक पहुंचने के लिए उपयोग की जाने वाली अभिव्यक्ति के रूप में ) एक अंतराल है।

बेशक, सवाल यह है कि क्या डिफ़ॉल्ट नियम "प्रतिद्वंद्वियों पर सदस्य का कार्य करना " होना चाहिए, जब तक कि उसके सभी संसाधनों का मालिक एक वस्तु वापस नहीं आनी चाहिए, जब तक कि कोई अच्छा कारण न हो । यह जिस उपनाम से वापस आता है वह कानूनी रूप से उपयोग किया जा सकता है, लेकिन आपके द्वारा अनुभव किए जा रहे तरीके से खतरनाक है: इसका उपयोग अपने "माता-पिता" के जीवनकाल को अस्थायी रूप से बढ़ाने के लिए नहीं किया जा सकता है:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

C ++ 2a में, मुझे लगता है कि आप इस (या इसी तरह) के आसपास काम करने वाले हैं जो निम्नानुसार है:

for( B b = bar(); auto i : b.a() )

ओपी के बजाय

for( auto i : bar().a() )

वर्कअराउंड मैन्युअल रूप से निर्दिष्ट करता है कि जीवनकाल bफॉर-लूप का संपूर्ण ब्लॉक है।

प्रस्ताव जो इस init-statement को पेश करता है

लाइव डेमो


हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.