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

18

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

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'è nulla che io possa fare per impedirlo (di cui sono a conoscenza)

Quindi la mia domanda è: come si difende qualcuno da questo?

L'unico approccio che viene in mente è scrivere sempre due versioni di qualsiasi metodo che accetta un std::string come 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));
    }
}

Questa è una soluzione comune e / o accettabile?

EDIT: alcuni hanno sottolineato che dovrei accettare const std::string& s anziché std::string s come parametro. Sono d'accordo. Ho modificato il post. Non penso che cambi la risposta però.

    
posta John Fitzpatrick 08.12.2013 - 19:57
fonte

7 risposte

20

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

Allo stesso modo si otterrebbe "riferimento null":

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

Colui che dereferenzia il puntatore nullo sta rendendo UB e ne è responsabile.

Inoltre, non puoi proteggerti dopo che è avvenuto un comportamento indefinito, perché l'ottimizzatore ha il pieno diritto di presumere che il comportamento indefinito non sia mai accaduto, quindi i controlli se c_str() è nullo possono essere ottimizzato.

    
risposta data 09.12.2013 - 20:24
fonte
2

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

Si noti che non sottintende che si dovrebbe normalmente fare 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
}
    
risposta data 13.12.2013 - 05:15
fonte
1

Ho anche riscontrato questo problema alcuni anni fa e ho trovato che fosse davvero una cosa spaventosa. Può accadere passando un nullptr o passando accidentalmente un int con il 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 occorrono 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'è.

    
risposta data 11.12.2013 - 01:39
fonte
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 che la chiamino, e la sciatteria di cui sei preoccupato causerà problemi prima che arrivi al tuo metodo, rendendo evidente, ciò che altri hanno sottolineato, che è l'errore del chiamante la tua.

    
risposta data 12.12.2013 - 19:23
fonte
1

Che ne dici se offri un sovraccarico con il parametro int ?

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

private:
    void myMethod(int);

Non è nemmeno necessario definire il sovraccarico. Se provi a chiamare myMethod(0) , verrà generato un errore del linker.

    
risposta data 13.12.2013 - 15:34
fonte
0

Il tuo metodo nel primo blocco di codice non verrà mai chiamato se proverai a chiamarlo con (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) 

Vedi? Non hai nulla di cui preoccuparti.

Ora se vuoi catturare un po 'più facilmente, dovresti semplicemente non usare char * , quindi il problema non si presenterà.

    
risposta data 08.12.2013 - 20:14
fonte
0

Se sei preoccupato che char * sia potenzialmente un puntatore nullo (ad esempio, restituisce da API C esterne) la risposta è usare una versione const char * della funzione invece di std :: string one. cioè.

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'utilizzo.

In generale, è meglio isolare le chiamate a API esterne e gli argomenti del marshall in valori validi.

    
risposta data 23.10.2018 - 12:54
fonte

Leggi altre domande sui tag