รับประกันตลอดอายุการใช้งานชั่วคราวใน C ++?


103

C ++ รับประกันอายุการใช้งานของตัวแปรชั่วคราวที่สร้างขึ้นภายในการเรียกใช้ฟังก์ชัน แต่ไม่ได้ใช้เป็นพารามิเตอร์หรือไม่ นี่คือคลาสตัวอย่าง:

class StringBuffer
{
public:
    StringBuffer(std::string & str) : m_str(str)
    {
        m_buffer.push_back(0);
    }
    ~StringBuffer()
    {
        m_str = &m_buffer[0];
    }
    char * Size(int maxlength)
    {
        m_buffer.resize(maxlength + 1, 0);
        return &m_buffer[0];
    }
private:
    std::string & m_str;
    std::vector<char> m_buffer;
};

และนี่คือวิธีที่คุณจะใช้:

// this is from a crusty old API that can't be changed
void GetString(char * str, int maxlength);

std::string mystring;
GetString(StringBuffer(mystring).Size(MAXLEN), MAXLEN);

ตัวทำลายสำหรับอ็อบเจ็กต์ StringBuffer ชั่วคราวจะถูกเรียกเมื่อใด ใช่ไหม:

  • ก่อนที่จะโทรไปที่ GetString?
  • หลังจาก GetString กลับมา?
  • ขึ้นอยู่กับคอมไพเลอร์?

ฉันรู้ว่า C ++ รับประกันว่าตัวแปรชั่วคราวภายในจะใช้ได้ตราบเท่าที่มีการอ้างอิง - สิ่งนี้ใช้กับออบเจ็กต์หลักเมื่อมีการอ้างอิงถึงตัวแปรสมาชิกหรือไม่

ขอบคุณ.


ทำไมไม่สืบทอดและโอเวอร์โหลดหรือสร้างฟังก์ชันส่วนกลาง ฉันจะสะอาดกว่านี้และคุณไม่ต้องสร้างชั้นเรียนเองเพื่อโทรหาสมาชิก
Jacek Ławrynowicz

1
หากคุณจะใช้สิ่งนี้คุณควรโทรm_str.reserve(maxlength)เข้าchar * Size(int maxlength)มิฉะนั้นผู้ทำลายอาจขว้างได้
Mankarse

คำตอบ:


109

ตัวทำลายสำหรับประเภทของจังหวะนั้นถูกเรียกที่ส่วนท้ายของนิพจน์เต็ม นั่นเป็นการแสดงออกภายนอกที่สุดซึ่งไม่ใช่ส่วนหนึ่งของนิพจน์อื่นใด นั่นคือในกรณีของคุณหลังจากที่ฟังก์ชันส่งคืนและมีการประเมินค่า ดังนั้นมันจะทำงานได้ดี

ในความเป็นจริงแล้วสิ่งที่ทำให้เทมเพลตนิพจน์ทำงานได้: สามารถเก็บการอ้างอิงถึงประเภทของจังหวะนั้นไว้ในนิพจน์

e = a + b * c / d

เพราะทุกชั่วคราวจะคงอยู่จนถึงการแสดงออก

x = y

ได้รับการประเมินอย่างสมบูรณ์ มีการอธิบายไว้ค่อนข้างรัดกุม12.2 Temporary objectsในมาตรฐาน


3
ฉันไม่เคยได้รับสำเนามาตรฐานมาก่อน ฉันควรให้ความสำคัญ
Mark Ransom

2
@JohannesSchaub: คือ "เต็มรูปแบบในการแสดงออก" ในกรณีนี้อะไร: printf("%s", strdup(std::string("$$$").c_str()) );ผมหมายถึงถ้าstrdup(std::string("$$$").c_str())จะมาเป็นการแสดงออกอย่างเต็มรูปแบบแล้วชี้ที่strdupเห็นเป็นที่ถูกต้อง ถ้าstd::string("$$$").c_str()เป็นนิพจน์แบบเต็มแสดงว่าตัวชี้ที่strdupเห็นนั้นไม่ถูกต้อง ! คุณช่วยอธิบายเพิ่มเติมจากตัวอย่างนี้ได้ไหม
Grim Fandango

2
@GrimFandango AIUI ทั้งหมดของคุณprintfคือการแสดงออกที่สมบูรณ์ ดังนั้นจึงstrdupเป็นการรั่วไหลของหน่วยความจำที่ไม่จำเป็น - คุณสามารถปล่อยให้มันพิมพ์c_str()โดยตรง
Josh Stone

1
"ประเภทของจังหวะนั้น" - แบบนั้นคืออะไร? ฉันจะทราบได้อย่างไรว่าชั่วคราวของฉันเป็น "แบบนั้น" ชั่วคราว
RM

@RM พอใช้. ฉันหมายถึง "สิ่งที่คุณสร้างขึ้นภายในอาร์กิวเมนต์ของฟังก์ชัน" ตามที่ทำในคำถาม
Johannes Schaub - litb

18

คำตอบของ litb นั้นถูกต้อง อายุการใช้งานของอ็อบเจ็กต์ชั่วคราว (หรือที่เรียกว่า rvalue) ถูกผูกไว้กับนิพจน์และ destructor สำหรับอ็อบเจ็กต์ชั่วคราวถูกเรียกที่ส่วนท้ายของนิพจน์แบบเต็มและเมื่อมีการเรียก destructor บน StringBuffer ตัวทำลายบน m_buffer ก็จะเป็นเช่นกัน เรียกว่า แต่ไม่ใช่ destructor บน m_str เนื่องจากเป็นการอ้างอิง

โปรดทราบว่า C ++ 0x เปลี่ยนแปลงสิ่งต่างๆเพียงเล็กน้อยเพราะเพิ่มการอ้างอิง rvalue และย้ายความหมาย โดยพื้นฐานแล้วโดยใช้พารามิเตอร์อ้างอิง rvalue (ระบุด้วย &&) ฉันสามารถ 'ย้าย' ค่า r เข้าไปในฟังก์ชัน (แทนที่จะคัดลอก) และอายุการใช้งานของ rvalue สามารถถูกผูกไว้กับวัตถุที่ย้ายเข้าไปไม่ใช่นิพจน์ มีบล็อกโพสต์ที่ดีจริงๆจากทีม MSVC ที่อธิบายรายละเอียดนี้อย่างละเอียดและฉันขอแนะนำให้คนอ่าน

ตัวอย่างการสอนสำหรับการย้าย rvalue เป็นสตริงชั่วคราวและฉันจะแสดงการมอบหมายในตัวสร้าง หากฉันมีคลาส MyType ที่มีตัวแปรสมาชิกสตริงสามารถเริ่มต้นด้วยค่า rvalue ในตัวสร้างดังนี้:

class MyType{
   const std::string m_name;
public:
   MyType(const std::string&& name):m_name(name){};
}

นี่เป็นสิ่งที่ดีเพราะเมื่อฉันประกาศอินสแตนซ์ของคลาสนี้ด้วยวัตถุชั่วคราว:

void foo(){
    MyType instance("hello");
}

สิ่งที่เกิดขึ้นคือเราหลีกเลี่ยงการคัดลอกและทำลายออบเจ็กต์ชั่วคราวและ "สวัสดี" จะวางไว้ในตัวแปรสมาชิกของอินสแตนซ์คลาสที่เป็นเจ้าของโดยตรง หากวัตถุมีน้ำหนักมากกว่า 'สตริง' การเรียกสำเนาและตัวทำลายเพิ่มเติมอาจมีนัยสำคัญ


1
เพื่อให้การย้ายทำงานได้ฉันคิดว่าคุณต้องวาง const และใช้ std :: move เช่น MyType (std :: string && name): m_name (std :: move (name)) {}
gast128


3

StringBuffer อยู่ในขอบเขตของ GetString มันควรจะถูกทำลายเมื่อสิ้นสุดขอบเขตของ GetString (เช่นเมื่อมันกลับมา) นอกจากนี้ฉันไม่เชื่อว่า C ++ จะรับประกันว่าตัวแปรจะมีอยู่ตราบเท่าที่มีการอ้างอิง

สิ่งต่อไปนี้ควรรวบรวม:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;

ฉันคิดว่าฉันคุยเกินจริงในการรับประกัน - สำหรับชั่วคราวในพื้นที่เท่านั้น แต่มันมีอยู่จริง
Mark Ransom

ฉันแก้ไขคำถามแล้ว จากคำตอบที่ให้มาตอนนี้ดูเหมือนว่าจะเป็นประเด็นที่สงสัย
Mark Ransom

ฉันยังคิดว่าการแก้ไขของคุณไม่ถูกต้อง: Object & obj = GetObj (); วัตถุ & GetObj () {return & Object (); } // ไม่ดี - จะทิ้งการอ้างอิงแบบห้อย
BigSandwich

1
เห็นได้ชัดว่าฉันอธิบายตัวเองได้แย่มากและฉันก็อาจจะไม่เข้าใจ 100% ด้วย ดูที่informit.com/guides/content.aspx?g=cplusplus&seqNum=198ซึ่งจะอธิบายและตอบคำถามเดิมของฉันด้วย
Mark Ransom

1
ขอบคุณสำหรับลิงก์ที่สมเหตุสมผลในตอนนี้
BigSandwich

3

ฉันเขียนเกือบจะเหมือนกันทุกประการ:

template <class C>
class _StringBuffer
{
    typename std::basic_string<C> &m_str;
    typename std::vector<C> m_buffer;

public:
    _StringBuffer(std::basic_string<C> &str, size_t nSize)
        : m_str(str), m_buffer(nSize + 1) { get()[nSize] = (C)0; }

    ~_StringBuffer()
        { commit(); }

    C *get()
        { return &(m_buffer[0]); }

    operator C *()
        { return get(); }

    void commit()
    {
        if (m_buffer.size() != 0)
        {
            size_t l = std::char_traits<C>::length(get());
            m_str.assign(get(), l);    
            m_buffer.resize(0);
        }
    }

    void abort()
        { m_buffer.resize(0); }
};

template <class C>
inline _StringBuffer<C> StringBuffer(typename std::basic_string<C> &str, size_t nSize)
    { return _StringBuffer<C>(str, nSize); }

ก่อนหน้ามาตรฐานแต่ละคอมไพเลอร์ทำไม่เหมือนกัน ฉันเชื่อว่าคู่มืออ้างอิงคำอธิบายประกอบแบบเก่าสำหรับ C ++ ระบุว่าจังหวะควรล้างที่ส่วนท้ายของขอบเขตดังนั้นคอมไพเลอร์บางตัวก็ทำเช่นนั้น เมื่อปลายปี 2546 ฉันพบว่าพฤติกรรมยังคงมีอยู่โดยค่าเริ่มต้นในคอมไพเลอร์ Forte C ++ ของ Sun ดังนั้น StringBuffer จึงไม่ทำงาน แต่ฉันจะประหลาดใจถ้าคอมไพเลอร์ปัจจุบันยังคงพังอยู่


น่ากลัวเหมือนกัน! ขอบคุณสำหรับคำเตือน - ที่แรกที่ฉันจะลองคือ VC ++ 6 ซึ่งไม่ทราบว่าเป็นไปตามมาตรฐาน ฉันจะคอยดูอย่างระมัดระวัง
Mark Ransom

ฉันจะเขียนคลาสเดิมใน VC ++ 6 ดังนั้นจึงไม่น่ามีปัญหา
Daniel Earwicker
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.