Wie schützt man am besten vor 0, die an die std :: string-Parameter übergeben werden?

20

Mir ist gerade etwas Störendes aufgefallen. Jedes Mal, wenn ich eine Methode geschrieben habe, die a std::stringals Parameter akzeptiert , habe ich mich für undefiniertes Verhalten geöffnet.

Zum Beispiel ist dies ...

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

... kann man so nennen ...

char* s = 0;
myMethod(s);

... und ich kann nichts dagegen tun (was mir bewusst ist).

Meine Frage lautet also: Wie verteidigt sich jemand dagegen?

Der einzige Ansatz, der in den Sinn kommt, besteht darin, immer zwei Versionen einer Methode zu schreiben, die einen std::stringals Parameter akzeptiert , wie folgt :

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

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

Ist das eine gängige und / oder akzeptable Lösung?

EDIT: Einige haben darauf hingewiesen, dass ich const std::string& sstatt std::string sals Parameter akzeptieren sollte . Genau. Ich habe den Beitrag geändert. Ich denke nicht, dass dies die Antwort ändert.

John Fitzpatrick
quelle
1
Hurra für undichte Abstraktionen! Ich bin kein C ++ - Entwickler, aber gibt es einen Grund, warum Sie die c_strEigenschaft des Zeichenfolgenobjekts nicht überprüfen konnten ?
Mason Wheeler
6
Das Zuweisen von 0 in den char * -Konstruktor ist undefiniertes Verhalten, daher ist es die Schuld des Anrufers
Ratschenfreak
4
@ratchetfreak Ich wusste nicht, dass char* s = 0das undefiniert ist. Ich habe es mindestens ein paar hundert Mal in meinem Leben gesehen (normalerweise in Form von char* s = NULL). Haben Sie eine Referenz, um das zu sichern?
John Fitzpatrick
3
Ich meinte zum std:string::string(char*)Konstrukteur
Ratschenfreak
2
Ich denke, Ihre Lösung ist in Ordnung, aber Sie sollten überlegen, überhaupt nichts zu tun. :-) Ihre Methode nimmt ganz klar einen String, übergibt keinen Nullzeiger, wenn sie eine gültige Aktion aufruft - für den Fall, dass ein Aufrufer versehentlich Nullen auf Methoden wie diese setzt, je eher sie in die Luft sprengt (eher als zum Beispiel in einer Protokolldatei gemeldet zu werden. Wenn es eine Möglichkeit gäbe, dies beim Kompilieren zu verhindern, sollten Sie dies tun, da ich es sonst belassen würde. MEINER BESCHEIDENEN MEINUNG NACH. (Übrigens, sind Sie sicher, dass Sie const std::string&für diesen Parameter keine nehmen können ...?)
Grimm The Opiner

Antworten:

21

Ich denke nicht, dass Sie sich schützen sollten. Es ist undefiniertes Verhalten auf der Seite des Anrufers. Sie sind es nicht, der anruft std::string::string(nullptr), was nicht erlaubt ist. Der Compiler lässt es kompilieren, aber es können auch andere undefinierte Verhalten kompiliert werden.

Der gleiche Weg wäre, "Null-Referenz" zu erhalten:

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

Derjenige, der den Nullzeiger dereferenziert, macht UB und ist dafür verantwortlich.

Darüber hinaus können Sie sich nicht schützen, nachdem ein undefiniertes Verhalten aufgetreten ist, da der Optimierer zu Recht davon ausgehen kann, dass das undefinierte Verhalten noch nie aufgetreten ist, sodass überprüft werden c_str()kann , ob null vorliegt.

Vlad
quelle
Es gibt einen wunderschön geschriebenen Kommentar, der fast dasselbe sagt, also musst du Recht haben. ;-)
Grimm The Opiner 10.12.13
2
Die Redewendung hier ist Schutz vor Murphy, nicht Machiavelli. Ein erfahrener böswilliger Programmierer kann Wege finden, schlecht geformte Objekte zu senden, um sogar Nullreferenzen zu erstellen, wenn sie sich anstrengen, aber das liegt an ihm. Sie können sich auch selbst in den Fuß schießen lassen, wenn sie es wirklich wollen. Das Beste, was Sie erwarten können, ist, versehentliche Fehler zu vermeiden. In diesem Fall kommt es in der Tat selten vor, dass jemand versehentlich ein Zeichen * s = 0 eingibt. zu einer Funktion, die nach einer gut geformten Zeichenfolge fragt.
YoungJohn
2

Der folgende Code gibt einen Kompilierungsfehler für einen expliziten Durchlauf 0und einen Laufzeitfehler für einen char*mit Wert an 0.

Beachten Sie, dass ich nicht impliziere, dass man dies normalerweise tun sollte, aber es kann zweifellos Fälle geben, in denen der Schutz vor dem Anruferfehler gerechtfertigt ist.

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
quelle
1

Ich bin auch vor ein paar Jahren auf dieses Problem gestoßen und fand es in der Tat sehr beängstigend. Dies kann passieren, indem Sie ein nullptroder versehentlich ein int mit dem Wert 0 übergeben. Es ist wirklich absurd:

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

Am Ende hat mich das jedoch nur ein paar Mal gestört. Und jedes Mal verursachte es einen sofortigen Absturz beim Testen meines Codes. Es sind also keine nächtlichen Sitzungen erforderlich, um das Problem zu beheben.

Ich halte eine Überlastung der Funktion für const char*eine gute Idee.

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

Ich wünschte, eine schönere Lösung wäre möglich. Gibt es aber nicht.

StackedCrooked
quelle
2
Aber Sie erhalten immer noch Laufzeitfehler, wenn für foo(0)und Kompilierungsfehler fürfoo(1)
Bryan Chen
1

Wie wäre es, wenn Sie die Signatur Ihrer Methode wie folgt ändern:

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

Auf diese Weise muss der Aufrufer eine Zeichenfolge erstellen, bevor er sie aufruft, und die Schlamperei, über die Sie sich Sorgen machen, führt zu Problemen, bevor Ihre Methode erreicht wird. Dies macht deutlich, was andere darauf hingewiesen haben, dass es sich um den Fehler des Aufrufers handelt, der nicht bei Ihnen liegt.

Justsalt
quelle
ja ich hätte es machen sollen const string& s, habe ich eigentlich vergessen. Aber bin ich trotzdem immer noch anfällig für undefiniertes Verhalten? Der Anrufer kann trotzdem ein 0, oder?
John Fitzpatrick
2
Wenn Sie einen nicht konstanten Verweis verwenden, kann der Aufrufer keine 0 mehr übergeben, da temporäre Objekte nicht zulässig sind. Die Verwendung Ihrer Bibliothek würde jedoch viel ärgerlicher werden (da temporäre Objekte nicht zulässig sind) und Sie geben die Konstanten-Korrektheit auf.
Josh Kelley
Ich habe einmal einen nicht konstanten Referenzparameter für eine Klasse verwendet, in der ich ihn speichern musste, und habe daher sichergestellt, dass keine Konvertierung oder temporäre
Übergabe erfolgt
1

Wie wäre es, wenn Sie bei Überlastung den Takes einen intParameter bereitstellen ?

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

private:
    void myMethod(int);

Sie müssen nicht einmal die Überlast definieren. Der Versuch, einen Anruf zu myMethod(0)tätigen, löst einen Linker-Fehler aus.

fredoverflow
quelle
1
Dies schützt nicht vor dem Code in der Frage, wo der 0einen char*Typ hat.
Ben Voigt
0

Ihre Methode im ersten Codeblock wird niemals aufgerufen, wenn Sie versuchen, sie mit einem aufzurufen (char *)0. C ++ versucht einfach, eine Zeichenfolge zu erstellen, und löst die Ausnahme für Sie aus. Hast Du es versucht?

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

Sehen? Du musst dir keine Sorgen machen.

Wenn Sie dies jetzt etwas eleganter einfangen möchten, sollten Sie es einfach nicht verwenden char *, dann tritt das Problem nicht auf.

vy32
quelle
4
das konstruieren eines std :: string mit einem nullpointer wird undefiniertes verhalten sein
ratschenfreak
2
die EXC_BAD_ACCESS Ausnahme klingt wie eine Null - Dereference , die Ihr Programm mit einem segfault an einem guten Tag abstürzen
Ratsche Freak
@ vy32 Ein Teil des Codes, den ich schreibe und der akzeptiert, std::stringgeht in Bibliotheken, die von anderen Projekten verwendet werden, bei denen ich nicht der Aufrufer bin. Ich suche nach einer Möglichkeit, die Situation angemessen zu handhaben und den Anrufer (möglicherweise mit einer Ausnahme) darüber zu informieren, dass er ein falsches Argument übergeben hat, ohne das Programm zum Absturz zu bringen. (OK, zugegeben, der Aufrufer behandelt möglicherweise keine von mir ausgelöste Ausnahme und das Programm stürzt trotzdem ab.)
John Fitzpatrick
2
@JohnFitzpatrick Sie können sich nicht vor einem Nullzeiger schützen, der an std :: string übergeben wird, es sei denn, Sie können das Standardkomitee davon überzeugen, dass es eine Ausnahme statt eines undefinierten Verhaltens darstellt
Ratschenfreak
@ratchetfreak In gewisser Weise denke ich, dass das die Antwort ist, nach der ich gesucht habe. Im Grunde muss ich mich schützen.
John Fitzpatrick
0

Wenn Sie befürchten, dass char * möglicherweise Null-Zeiger sind (z. B. Rückgaben von externen C-APIs), verwenden Sie statt der std :: string-Version eine const char * -Version der Funktion. dh

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

Natürlich benötigen Sie auch die std :: string-Version, wenn Sie die Verwendung dieser zulassen möchten.

Im Allgemeinen ist es am besten, Aufrufe an externe APIs und Marshall-Argumente in gültige Werte zu isolieren.

Superfly Jon
quelle