Dijamin seumur hidup sementara di C ++?


103

Apakah C ++ memberikan jaminan seumur hidup variabel sementara yang dibuat dalam panggilan fungsi tetapi tidak digunakan sebagai parameter? Berikut contoh kelasnya:

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;
};

Dan inilah cara Anda menggunakannya:

// 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);

Kapan destruktor untuk objek StringBuffer sementara dipanggil? Apakah itu:

  • Sebelum panggilan ke GetString?
  • Setelah GetString kembali?
  • Tergantung kompiler?

Saya tahu bahwa C ++ menjamin bahwa variabel temporer lokal akan valid selama ada referensi ke sana - apakah ini berlaku untuk objek induk ketika ada referensi ke variabel anggota?

Terima kasih.


mengapa tidak mewarisi dan membebani atau membuat fungsi global? saya akan lebih bersih dan Anda tidak perlu membuat kelas hanya untuk memanggil anggota.
Jacek Ławrynowicz

1
Jika Anda akan menggunakan ini, Anda harus menelepon m_str.reserve(maxlength)di char * Size(int maxlength), jika tidak destructor bisa melempar.
Mankarse

Jawaban:


109

Penghancur untuk temporer semacam itu dipanggil di akhir ekspresi penuh. Itu adalah ekspresi paling luar yang bukan merupakan bagian dari ekspresi lain. Itu terjadi dalam kasus Anda setelah fungsi kembali dan nilainya dievaluasi. Jadi, semuanya akan bekerja dengan baik.

Sebenarnya itulah yang membuat templat ekspresi berfungsi: Mereka dapat menyimpan referensi ke temporer semacam itu dalam ekspresi seperti

e = a + b * c / d

Karena setiap sementara akan bertahan sampai ekspresi

x = y

Dievaluasi sepenuhnya. Ini cukup singkat dijelaskan dalam 12.2 Temporary objectsStandar.


3
Saya tidak pernah benar-benar mendapatkan salinan standar. Saya harus menjadikannya sebagai prioritas.
Markus Tebusan

2
@JohannesSchaub: Apa yang dimaksud dengan "ekspresi penuh" dalam kasus ini:? printf("%s", strdup(std::string("$$$").c_str()) );Maksud saya, jika strdup(std::string("$$$").c_str())diambil sebagai ekspresi penuh, maka penunjuk yang strdupdilihat adalah valid . Jika std::string("$$$").c_str()ekspresi penuh, maka penunjuk yang strdupmelihat tidak valid ! Bisakah Anda menjelaskan sedikit lebih banyak berdasarkan contoh ini?
Grim Fandango

2
@GrimFandango AIUI seluruh Anda printfadalah ekspresi penuh. Jadi, strdupini adalah kebocoran memori yang tidak perlu - Anda dapat membiarkannya mencetak c_str()secara langsung.
Josh Stone

1
"Temporer semacam itu" - Apa itu? Bagaimana saya bisa tahu apakah sementara saya adalah "semacam itu" sementara?
RM

@R cukup adil. Maksud saya "yang Anda buat dalam argumen fungsi", seperti yang dilakukan dalam pertanyaan.
Johannes Schaub - litb

18

jawaban litb akurat. Umur objek sementara (juga dikenal sebagai nilai r) terkait dengan ekspresi dan destruktor untuk objek sementara dipanggil di akhir ekspresi penuh dan ketika destruktor pada StringBuffer dipanggil, destruktor di m_buffer juga akan dipanggil, tetapi bukan destruktor pada m_str karena ini adalah referensi.

Perhatikan bahwa C ++ 0x mengubah hal-hal hanya sedikit karena ia menambahkan referensi rvalue dan memindahkan semantik. Pada dasarnya dengan menggunakan parameter referensi nilai r (dinotasikan dengan &&) saya dapat 'memindahkan' nilai r ke dalam fungsi (alih-alih menyalinnya) dan umur nilai r dapat terikat ke objek yang dimasukinya, bukan ekspresi. Ada sebuah posting blog yang sangat bagus dari tim MSVC yang menjelaskan hal ini dengan sangat rinci dan saya mendorong orang untuk membacanya.

Contoh pedagogis untuk memindahkan rvalue adalah string sementara dan saya akan menunjukkan tugas di konstruktor. Jika saya memiliki kelas MyType yang berisi variabel anggota string, itu dapat diinisialisasi dengan rvalue dalam konstruktor seperti:

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

Ini bagus karena ketika saya mendeklarasikan instance kelas ini dengan objek sementara:

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

yang terjadi adalah bahwa kita menghindari menyalin dan menghancurkan objek sementara dan "hello" ditempatkan langsung di dalam variabel anggota instance kelas pemilik. Jika objek lebih berat daripada 'string' maka salinan tambahan dan pemanggilan destruktor bisa menjadi signifikan.


1
Untuk membuat pemindahan bekerja, saya pikir Anda perlu menjatuhkan const dan menggunakan std :: move seperti MyType (std :: string && name): m_name (std :: move (name)) {}
gast128


3

StringBuffer berada dalam cakupan GetString. Ini harus dihancurkan di akhir cakupan GetString (yaitu saat kembali). Juga, saya tidak percaya bahwa C ++ akan menjamin bahwa variabel akan ada selama ada referensi.

Berikut ini harus dikompilasi:

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

Saya pikir saya melebih-lebihkan jaminan - ini hanya untuk temporer lokal. Tapi itu memang ada.
Mark Ransom

Saya telah mengedit pertanyaan itu. Berdasarkan jawaban yang diberikan sejauh ini, tampaknya ini menjadi poin yang diperdebatkan.
Tandai Tebusan

Saya masih merasa hasil edit Anda tidak benar: Object & obj = GetObj (); Object & GetObj () {return & Object (); } // buruk - akan meninggalkan referensi yang menggantung.
BigSandwich

1
Jelas saya melakukan pekerjaan yang buruk dalam menjelaskan diri saya sendiri, dan saya mungkin juga tidak mengerti 100%. Lihat informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - ini menjelaskan dan menjawab pertanyaan asli saya juga.
Mark Ransom

1
Terima kasih, untuk tautannya, itu masuk akal sekarang.
BigSandwich

3

Saya menulis kelas yang hampir persis sama:

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); }

Sebelum standar, setiap kompilator melakukannya secara berbeda. Saya percaya Manual Referensi Beranotasi lama untuk C ++ menetapkan bahwa sementara harus dibersihkan di akhir ruang lingkup, sehingga beberapa kompiler melakukannya. Hingga tahun 2003, saya menemukan bahwa perilaku tersebut masih ada secara default di kompiler Forte C ++ Sun, jadi StringBuffer tidak berfungsi. Tetapi saya akan heran jika ada kompiler saat ini yang masih rusak.


Mengerikan betapa miripnya mereka! Terima kasih atas peringatannya - tempat pertama saya akan mencobanya adalah VC ++ 6, yang tidak dikenal karena kepatuhan standarnya. Saya akan mengawasi dengan cermat.
Tandai Tebusan

Saya akan menulis kelas awalnya di VC ++ 6 jadi seharusnya tidak menjadi masalah.
Daniel Earwicker
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.