Bagaimana cara terbaik melindungi dari 0 diteruskan ke parameter std :: string?

20

Saya baru saja menyadari sesuatu yang mengganggu. Setiap kali saya menulis metode yang menerima std::stringsebagai parameter, saya membuka diri terhadap perilaku yang tidak jelas.

Sebagai contoh, ini ...

void myMethod(const std::string& s) { 
    /* Do something with s. */
}

... bisa disebut seperti ini ...

char* s = 0;
myMethod(s);

... dan tidak ada yang bisa saya lakukan untuk mencegahnya (yang saya sadari).

Jadi pertanyaan saya adalah: Bagaimana seseorang mempertahankan diri dari hal ini?

Satu-satunya pendekatan yang terlintas dalam pikiran adalah untuk selalu menulis dua versi dari metode apa pun yang menerima std::stringparameter, seperti ini:

void myMethod(const std::string& s) {
    /* Do something. */
}

void myMethod(char* s) {
    if (s == 0) {
        throw std::exception("Null passed.");
    } else {
        myMethod(string(s));
    }
}

Apakah ini solusi yang umum dan / atau dapat diterima?

EDIT: Beberapa telah menunjukkan bahwa saya harus menerima const std::string& salih-alih std::string ssebagai parameter. Saya setuju. Saya memodifikasi posting. Saya tidak berpikir itu mengubah jawabannya.

John Fitzpatrick
sumber
1
Hore untuk abstraksi bocor! Saya bukan pengembang C ++, tetapi apakah ada alasan mengapa Anda tidak dapat memeriksa c_strproperti objek string ?
Mason Wheeler
6
hanya menetapkan 0 ke dalam char * constructor adalah perilaku yang tidak terdefinisi, jadi itu benar
ratchet freak
4
@ scratchetfreak Saya tidak tahu itu char* s = 0tidak terdefinisi. Saya telah melihatnya setidaknya beberapa ratus kali dalam hidup saya (biasanya dalam bentuk char* s = NULL). Apakah Anda memiliki referensi untuk mendukungnya?
John Fitzpatrick
3
Saya bermaksud untuk std:string::string(char*)konstruktor
ratchet freak
2
Saya pikir solusi Anda baik-baik saja, tetapi Anda harus mempertimbangkan untuk tidak melakukan apa-apa sama sekali. :-) Metode Anda cukup jelas mengambil string, sama sekali tidak melewatkan null pointer ketika menyebutnya tindakan yang valid - jika penelepon secara tidak sengaja membuat nulls tentang metode seperti ini, semakin cepat ia meledak pada mereka (bukan daripada dilaporkan dalam file log, misalnya) semakin baik. Jika ada cara untuk mencegah sesuatu ini pada waktu kompilasi maka Anda harus melakukannya, kalau tidak saya akan meninggalkannya. MENURUT OPINI SAYA. (BTW, apakah Anda yakin tidak dapat mengambil const std::string&parameter untuk itu ...?)
Grimm The Opiner

Jawaban:

21

Saya tidak berpikir Anda harus melindungi diri sendiri. Itu perilaku yang tidak ditentukan di pihak pemanggil. Bukan Anda, itu penelepon yang memanggil std::string::string(nullptr), yang tidak diizinkan. Compiler memungkinkannya dikompilasi, tetapi memungkinkan perilaku tidak terdefinisi lainnya dikompilasi juga.

Cara yang sama akan mendapatkan "referensi nol":

int* p = nullptr;
f(*p);
void f(int& x) { x = 0; /* bang! */ }

Orang yang mereferensi null pointer membuat UB dan bertanggung jawab untuk itu.

Selain itu, Anda tidak dapat melindungi diri sendiri setelah perilaku tidak terdefinisi terjadi, karena pengoptimal berada dalam hak penuh untuk mengasumsikan bahwa perilaku tidak terdefinisi tidak pernah terjadi, sehingga pemeriksaan jika c_str()nol dapat dioptimalkan.

Vlad
sumber
Ada komentar yang ditulis dengan indah yang mengatakan hal yang sama, jadi Anda pasti benar. ;-)
Grimm The Opiner
2
Ungkapan di sini adalah melindungi terhadap Murphy, bukan Machiavelli. Seorang programmer jahat yang berpengalaman dapat menemukan cara untuk mengirim benda-benda yang dibentuk dengan buruk bahkan untuk membuat referensi nol jika mereka berusaha cukup keras, tetapi itu adalah pekerjaan mereka sendiri dan Anda mungkin juga membiarkan mereka menembak diri mereka sendiri di kaki jika mereka benar-benar menginginkannya. Yang terbaik yang dapat Anda lakukan adalah mencegah kesalahan yang tidak disengaja. Dalam kasus ini, memang jarang ada seseorang yang secara tidak sengaja memasukkan karakter * s = 0; ke fungsi yang meminta string yang terbentuk dengan baik.
YoungJohn
2

Kode di bawah ini memberikan kegagalan kompilasi untuk pass eksplisit 0, dan kegagalan run-time untuk char*nilai with 0.

Perhatikan bahwa saya tidak menyiratkan bahwa seseorang seharusnya melakukan ini secara normal, tetapi tidak diragukan lagi ada kasus-kasus di mana perlindungan dari kesalahan pemanggil dibenarkan.

struct Test
{
    template<class T> void myMethod(T s);
};

template<> inline void Test::myMethod(const std::string& s)
{
    std::cout << "Cool " << std::endl;
}

template<> inline void Test::myMethod(const char* s)
{
    if (s != 0)
        myMethod(std::string(s));
    else
    {
        throw "Bad bad bad";
    }
}

template<class T> inline void Test::myMethod(T  s)
{
    myMethod(std::string(s));
    const bool ok = !std::is_same<T,int>::value;
    static_assert(ok, "oops");
}

int main()
{
    Test t;
    std::string s ("a");
    t.myMethod("b");
    const char* c = "c";
    t.myMethod(c);
    const char* d = 0;
    t.myMethod(d); // run time exception
    t.myMethod(0); // Compile failure
}
Keith
sumber
1

Saya juga mengalami masalah ini beberapa tahun yang lalu dan saya merasa ini adalah hal yang sangat menakutkan. Itu bisa terjadi dengan melewati nullptratau secara tidak sengaja melewati int dengan nilai 0. Ini benar-benar tidak masuk akal:

std::string s(1); // compile error
std::string s(0); // runtime error

Namun, pada akhirnya ini hanya menyadap saya beberapa kali. Dan setiap kali itu menyebabkan crash langsung ketika menguji kode saya. Jadi tidak ada sesi sepanjang malam akan diperlukan untuk memperbaikinya.

Saya pikir kelebihan fungsi const char*adalah ide yang bagus.

void foo(std::string s)
{
    // work
}

void foo(const char* s) // use const char* rather than char* (binds to string literals)
{
    assert(s); // I would use assert or std::abort in case of nullptr . 
    foo(std::string(s));
}

Saya berharap solusi yang lebih baik dimungkinkan. Namun, tidak ada.

StackedCrooked
sumber
2
tetapi Anda masih mendapatkan kesalahan runtime ketika untuk foo(0)dan mengkompilasi kesalahan untukfoo(1)
Bryan Chen
1

Bagaimana dengan mengubah tanda tangan metode Anda menjadi:

void myMethod(std::string& s) // maybe throw const in there too.

Dengan cara itu pemanggil harus membuat string sebelum mereka menyebutnya, dan kecerobohan yang Anda khawatirkan akan menyebabkan masalah sebelum sampai ke metode Anda, membuat jelas, apa yang orang lain tunjukkan, bahwa itu adalah kesalahan pemanggil bukan milik Anda.

Justsalt
sumber
ya saya harus membuatnya const string& s, saya benar-benar lupa. Namun demikian, bukankah saya masih rentan terhadap perilaku yang tidak terdefinisi? Penelepon masih bisa melewati 0, kan?
John Fitzpatrick
2
Jika Anda menggunakan referensi non-const, maka penelepon tidak dapat lagi melewati 0, karena objek sementara tidak akan diizinkan. Namun, menggunakan perpustakaan Anda akan menjadi jauh lebih menjengkelkan (karena objek sementara tidak akan diizinkan) dan Anda menyerah pada kebenaran const.
Josh Kelley
Saya pernah menggunakan parameter referensi non-const ke kelas di mana saya harus dapat menyimpannya dan memastikan karena itu tidak ada konversi atau sementara disahkan.
CashCow
1

Bagaimana kalau Anda memberikan overload yang dibutuhkan int parameter?

public:
    void myMethod(const std::string& s)
    { 
        /* Do something with s. */
    }    

private:
    void myMethod(int);

Anda bahkan tidak perlu mendefinisikan overload. Mencoba menelepon myMethod(0)akan memicu kesalahan tautan.

fredoverflow
sumber
1
Ini tidak melindungi terhadap kode dalam pertanyaan, di mana 0memiliki char*tipe.
Ben Voigt
0

Metode Anda di blok kode pertama tidak akan pernah dipanggil jika Anda mencoba menyebutnya dengan a (char *)0. C ++ hanya akan mencoba membuat string dan itu akan membuang pengecualian untuk Anda. Apakah kamu sudah mencobanya?

#include <cstdlib>
#include <iostream>

void myMethod(std::string s) {
    std::cout << "s=" << s << "\n";
}

int main(int argc,char **argv) {
    char *s = 0;
    myMethod(s);
    return(0);
}


$ g++ -g -o x x.cpp 
$ lldb x 
(lldb) run
Process 2137 launched: '/Users/simsong/x' (x86_64)
Process 2137 stopped
* thread #1: tid = 0x49b8, 0x00007fff99bf9812 libsystem_c.dylib`strlen + 18, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x00007fff99bf9812 libsystem_c.dylib`strlen + 18
libsystem_c.dylib`strlen + 18:
-> 0x7fff99bf9812:  pcmpeqb (%rdi), %xmm0
   0x7fff99bf9816:  pmovmskb %xmm0, %esi
   0x7fff99bf981a:  andq   $15, %rcx
   0x7fff99bf981e:  orq    $-1, %rax
(lldb) bt
* thread #1: tid = 0x49b8, 0x00007fff99bf9812 libsystem_c.dylib`strlen + 18, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x00007fff99bf9812 libsystem_c.dylib`strlen + 18
    frame #1: 0x000000010000077a x`main [inlined] std::__1::char_traits<char>::length(__s=0x0000000000000000) + 122 at string:644
    frame #2: 0x000000010000075d x`main [inlined] std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::basic_string(this=0x00007fff5fbff548, __s=0x0000000000000000) + 8 at string:1856
    frame #3: 0x0000000100000755 x`main [inlined] std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::basic_string(this=0x00007fff5fbff548, __s=0x0000000000000000) at string:1857
    frame #4: 0x0000000100000755 x`main(argc=1, argv=0x00007fff5fbff5e0) + 85 at x.cpp:10
    frame #5: 0x00007fff92ea25fd libdyld.dylib`start + 1
(lldb) 

Lihat? Anda tidak perlu khawatir.

Sekarang jika Anda ingin menangkap ini sedikit lebih anggun, Anda seharusnya tidak menggunakannya char *, maka masalahnya tidak akan muncul.

ay32
sumber
4
membangun std :: string dengan nullpointer akan menjadi perilaku yang tidak terdefinisi
ratchet freak
2
pengecualian EXC_BAD_ACCESS terdengar seperti null dereference yang akan merusak program Anda dengan segfault pada hari yang baik
ratchet freak
@ vy32 Beberapa kode yang saya tulis yang diterima std::stringmasuk ke perpustakaan yang digunakan oleh proyek lain di mana saya bukan penelepon. Saya mencari cara untuk menangani situasi dengan anggun dan memberi tahu penelepon (mungkin dengan pengecualian) bahwa mereka mengeluarkan argumen yang salah tanpa merusak program. (Oke, memang, penelepon mungkin tidak menangani pengecualian yang saya lemparkan dan programnya akan macet.)
John Fitzpatrick
2
@JohnFitzpatrick Anda tidak akan dapat melindungi diri Anda dari nullpointer yang diteruskan ke std :: string kecuali Anda dapat meyakinkan comity standar untuk menjadikannya pengecualian dan bukan perilaku yang tidak ditentukan
ratchet freak
@ scratchetfreak Dengan cara saya pikir itu jawaban yang saya cari. Jadi pada dasarnya saya harus melindungi diri saya sendiri.
John Fitzpatrick
0

Jika Anda khawatir char * berpotensi pointer nol (mis. Pengembalian dari API C eksternal) jawabannya adalah dengan menggunakan versi const char * dari fungsi alih-alih std :: string one. yaitu

void myMethod(const char* c) { 
    std::string s(c ? c : "");
    /* Do something with s. */
}

Tentu saja Anda akan memerlukan versi std :: string juga jika Anda ingin mengizinkannya untuk digunakan.

Secara umum yang terbaik adalah mengisolasi panggilan ke API eksternal dan argumen marshal ke dalam nilai yang valid.

Superfly Jon
sumber