Come proteggere al meglio da 0 passato ai parametri std :: string?


20

Ho appena realizzato qualcosa di inquietante. Ogni volta che ho scritto un metodo che accetta std::stringun parametro come parametro, mi sono aperto a comportamenti indefiniti.

Ad esempio, questo ...

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

... può essere chiamato così ...

char* s = 0;
myMethod(s);

... e non c'è niente che io possa fare per prevenirlo (di cui sono a conoscenza).

Quindi la mia domanda è: in che modo qualcuno si difende da questo?

L'unico approccio che viene in mente è quello di scrivere sempre due versioni di qualsiasi metodo che accetta un std::stringcome parametro, come questo:

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

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

È una soluzione comune e / o accettabile?

EDIT: Alcuni hanno sottolineato che dovrei accettare const std::string& sinvece std::string scome parametro. Sono d'accordo. Ho modificato il post. Non penso che cambi la risposta però.


1
Evviva per astrazioni che perdono! Non sono uno sviluppatore C ++, ma c'è qualche motivo per cui non è possibile controllare la c_strproprietà dell'oggetto stringa ?
Mason Wheeler,

6
assegnare semplicemente 0 al costruttore char * è un comportamento indefinito, quindi è davvero colpa dei chiamanti
maniaco del cricchetto

4
@ratchetfreak Non sapevo che char* s = 0non fosse definito. L'ho visto almeno alcune centinaia di volte nella mia vita (di solito sotto forma di char* s = NULL). Hai un riferimento per eseguire il backup?
John Fitzpatrick,

3
Volevo dire al std:string::string(char*)costruttore
maniaco del cricchetto l'

2
Penso che la tua soluzione vada bene, ma dovresti considerare di non fare nulla. :-) Il tuo metodo prende abbastanza chiaramente una stringa, in nessun modo passa un puntatore nullo quando lo chiama un'azione valida - nel caso in cui un chiamante stia accidentalmente portando a zero valori come questo prima che esploda su di essi (piuttosto che essere segnalato in un file di registro, ad esempio), meglio è. Se ci fosse un modo per impedire qualcosa di simile al momento della compilazione, allora dovresti farlo, altrimenti lo lascerei. A PARER MIO. (A proposito, sei sicuro di non poter prendere un const std::string&per quel parametro ...?)
Grimm The Opiner,

Risposte:


21

Non penso che dovresti proteggerti. È un comportamento indefinito dal lato del chiamante. Non sei tu, è il chiamante che chiama std::string::string(nullptr), che è ciò che non è permesso. Il compilatore consente di compilarlo, ma consente anche di compilare altri comportamenti indefiniti.

Allo stesso modo sarebbe ottenere "riferimento null":

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

Chi dereferenzia il puntatore null sta creando UB e ne è responsabile.

Inoltre, non è possibile proteggersi dopo che si è verificato un comportamento indefinito, poiché l'ottimizzatore ha il pieno diritto di presumere che il comportamento indefinito non si sia mai verificato, quindi i controlli se c_str()è nullo possono essere ottimizzati.


C'è un commento ben scritto che dice più o meno la stessa cosa, quindi devi avere ragione. ;-)
Grimm The Opiner il

2
La frase qui è protetta contro Murphy, non Machiavelli. Un programmatore malintenzionato esperto può trovare il modo di inviare oggetti malformati per persino creare riferimenti null se si sforzano abbastanza, ma è quello che fanno loro e potresti anche lasciarli sparare ai piedi se lo vogliono davvero. Il meglio che ci si può aspettare è prevenire errori accidentali. In questo caso è raro infatti che qualcuno passi accidentalmente un carattere * s = 0; a una funzione che richiede una stringa ben formata.
Young John

2

Il codice seguente fornisce un errore di compilazione per un passaggio esplicito di 0e un errore di runtime per un char*valore with 0.

Si noti che non sottintendo che si dovrebbe fare normalmente questo, ma senza dubbio ci possono essere casi in cui la protezione dall'errore del chiamante è giustificata.

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
}

1

Ho anche riscontrato questo problema alcuni anni fa e l'ho trovato davvero una cosa molto spaventosa. Può succedere passando un nullptro passando accidentalmente un int con valore 0. È davvero assurdo:

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

Tuttavia, alla fine, questo mi ha infastidito solo un paio di volte. E ogni volta ha causato un arresto immediato durante il test del mio codice. Quindi non saranno necessarie sessioni notturne per risolverlo.

Penso che sovraccaricare la funzione con const char*sia una buona idea.

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

Vorrei che fosse possibile una soluzione migliore. Tuttavia, non c'è.


2
ma ricevi ancora errore di runtime quando per foo(0)e compila l'errore perfoo(1)
Bryan Chen l'

1

Che ne dici di cambiare la firma del tuo metodo in:

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

In questo modo il chiamante deve creare una stringa prima di chiamarla, e la sciattezza di cui sei preoccupato causerà problemi prima che arrivi al tuo metodo, rendendo evidente, ciò che gli altri hanno sottolineato, che è l'errore del chiamante non tuo.


sì, avrei dovuto farlo const string& s, in realtà mi ero dimenticato. Ma anche così, non sono ancora vulnerabile a comportamenti indefiniti? Il chiamante può comunque passare un 0, giusto?
John Fitzpatrick,

2
Se si utilizza un riferimento non const, il chiamante non può più passare 0, poiché gli oggetti temporanei non saranno consentiti. Tuttavia, l'uso della tua libreria diventerebbe molto più fastidioso (perché gli oggetti temporanei non saranno ammessi) e ti arrendi alla correttezza costante.
Josh Kelley,

Una volta ho usato un parametro di riferimento non const in una classe in cui dovevo essere in grado di memorizzarlo e mi assicuravo quindi che non vi fosse alcuna conversione o passaggio temporaneo.
CashCow

1

Che ne dici di fornire in sovraccarico il prende un intparametro?

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

private:
    void myMethod(int);

Non è nemmeno necessario definire il sovraccarico. Il tentativo di chiamare myMethod(0)attiverà un errore del linker.


1
Questo non protegge dal codice nella domanda, dove 0ha un char*tipo.
Ben Voigt,

0

Il tuo metodo nel primo blocco di codice non verrà mai chiamato se provi a chiamarlo con a (char *)0. C ++ proverà semplicemente a creare una stringa e genererà l'eccezione per te. Hai provato?

#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) 

Vedere? Non hai nulla di cui preoccuparti.

Ora, se vuoi prenderlo un po 'più con grazia, non dovresti semplicemente usarlo char *, quindi il problema non si presenterà.


4
costruire uno std :: string con un nullpointer sarà un comportamento indefinito
maniaco del cricchetto

2
l'eccezione EXC_BAD_ACCESS suona come una null dereference che farà crashare il tuo programma con un segfault in una buona giornata
maniaco del cricchetto

@ vy32 Parte del codice che scrivo che accetta std::stringva nelle librerie utilizzate da altri progetti in cui non sono il chiamante. Sto cercando un modo per gestire con garbo la situazione e informare il chiamante (forse con un'eccezione) che hanno superato un argomento errato senza arrestare il programma. (OK, garantito, il chiamante potrebbe non gestire un'eccezione che lancio e il programma andrà in crash comunque.)
John Fitzpatrick,

2
@JohnFitzpatrick non sarai in grado di proteggerti da un nullpointer passato in std :: string a meno che tu non riesca a convincere i comity standard di avere un'eccezione invece di un comportamento indefinito
maniaco del cricco

@ratchetfreak In un certo senso penso che sia la risposta che stavo cercando. Quindi in pratica devo proteggermi.
John Fitzpatrick,

0

Se sei preoccupato che char * sia potenzialmente un puntatore nullo (es. Ritorni da API C esterne) la risposta è usare una versione const char * della funzione invece di quella std :: string. vale a dire

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

Ovviamente avrai bisogno anche della versione std :: string se vuoi permetterne l'uso.

In generale, è meglio isolare le chiamate ad API esterne e argomenti marshal in valori validi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.