Jak najlepiej chronić od zera przekazanego do parametrów std :: string?

20

Właśnie zdałem sobie sprawę z czegoś niepokojącego. Za każdym razem, gdy piszę metodę, która przyjmuje parametr std::stringjako parametr, otwieram się na niezdefiniowane zachowanie.

Na przykład to ...

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

... można tak nazwać ...

char* s = 0;
myMethod(s);

... i nie mogę nic na to poradzić (jestem tego świadomy).

Więc moje pytanie brzmi: jak ktoś się przed tym broni?

Jedyne podejście, jakie przychodzi mi na myśl, to zawsze pisać dwie wersje dowolnej metody, która akceptuje std::stringjako parametr, jak poniżej:

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

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

Czy jest to powszechne i / lub akceptowalne rozwiązanie?

EDYCJA: Niektórzy zauważyli, że powinienem zaakceptować const std::string& szamiast std::string sparametru. Zgadzam się. Zmodyfikowałem post. Nie sądzę, że to zmienia odpowiedź.

John Fitzpatrick
źródło
1
Brawo dla nieszczelnych abstrakcji! Nie jestem programistą C ++, ale czy istnieje jakiś powód, dla którego nie mogłeś sprawdzić właściwości obiektu ciągu c_str?
Mason Wheeler,
6
po prostu przypisanie 0 do konstruktora char * jest niezdefiniowanym zachowaniem, więc to naprawdę wina wzywającego
maniak zapadkowy
4
@ratchetfreak Nie wiedziałem, że char* s = 0jest niezdefiniowany. Widziałem to co najmniej kilkaset razy w moim życiu (zwykle w formie char* s = NULL). Czy masz odniesienie, aby to zrobić?
John Fitzpatrick,
3
Miałem na myśli std:string::string(char*)konstruktora
maniaka zapadkowego
2
Myślę, że twoje rozwiązanie jest w porządku, ale powinieneś rozważyć, aby w ogóle nic nie robić. :-) Twoja metoda dość wyraźnie przyjmuje ciąg znaków, w żadnym wypadku nie przekazuje wskaźnika zerowego podczas nazywania go prawidłową akcją - w przypadku, gdy osoba dzwoniąca przypadkowo rzuci null na metody takie jak ta, tym szybciej na nich wybuchnie (raczej niż na przykład zgłaszanie w pliku dziennika), tym lepiej. Jeśli istnieje sposób, aby temu zapobiec w czasie kompilacji, powinieneś to zrobić, w przeciwnym razie zostawiłbym to. MOIM ZDANIEM. (BTW, czy jesteś pewien, że nie mógłbyś brać const std::string&tego parametru ...?)
Grimm The Opiner

Odpowiedzi:

21

Nie sądzę, że powinieneś się chronić. Jest to niezdefiniowane zachowanie po stronie dzwoniącego. To nie ty, to dzwoniący dzwoni std::string::string(nullptr), co jest niedozwolone. Kompilator pozwala na kompilację, ale pozwala także na kompilację innych niezdefiniowanych zachowań.

W ten sam sposób można uzyskać „odwołanie zerowe”:

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

Ten, kto wyrejestrowuje wskaźnik zerowy, tworzy UB i jest za to odpowiedzialny.

Co więcej, nie możesz się zabezpieczyć po wystąpieniu niezdefiniowanego zachowania, ponieważ optymalizator ma pełne prawo założyć, że niezdefiniowane zachowanie nigdy się nie zdarzyło, więc można sprawdzić, czy c_str()wartość zerowa jest zerowa.

Vlad
źródło
Jest pięknie napisany komentarz, który mówi to samo, więc musisz mieć rację. ;-)
Grimm The Opiner
2
Zwrot tutaj chroni przed Murphym, a nie Machiavellim. Dobrze zorientowany złośliwy programista może znaleźć sposoby na wysyłanie źle sformułowanych obiektów, aby nawet tworzyć zerowe referencje, jeśli spróbują wystarczająco mocno, ale to ich własna robota i równie dobrze możesz pozwolić im strzelić sobie w stopę, jeśli naprawdę tego chcą. Najlepsze, czego można oczekiwać, to zapobieganie przypadkowym błędom. W tym przypadku rzeczywiście rzadko zdarza się, aby ktoś przypadkowo przekazał znak * * = 0; do funkcji, która prosi o dobrze uformowany ciąg.
YoungJn
2

Poniższy kod podaje błąd kompilacji dla jawnego przekazania 0i błąd wykonania dla char*wartości z 0.

Zauważ, że nie sugeruję, że normalnie należy to zrobić, ale bez wątpienia mogą istnieć przypadki, w których ochrona przed błędem dzwoniącego jest uzasadniona.

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
źródło
1

Problem ten spotkałem również kilka lat temu i okazało się, że jest to bardzo przerażająca rzecz. Może się to zdarzyć, przechodząc przez nullptrlub przypadkowo przekazując liczbę całkowitą o wartości 0. To naprawdę absurdalne:

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

Jednak w końcu tylko to mnie wkurzyło. I za każdym razem powodowało to natychmiastową awarię podczas testowania mojego kodu. Aby to naprawić, nie będą potrzebne nocne sesje.

Myślę, że przeciążenie tej funkcji const char*jest dobrym pomysłem.

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

Chciałbym, żeby możliwe było lepsze rozwiązanie. Jednak nie ma.

StackedCrooked
źródło
2
ale nadal pojawia się błąd czasu wykonania po błędzie foo(0)i błąd kompilacji dlafoo(1)
Bryan Chen,
1

Co powiesz na zmianę podpisu metody na:

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

W ten sposób osoba dzwoniąca musi utworzyć ciąg, zanim go wywoła, a niechlujność, o którą się martwisz, spowoduje problemy, zanim dotrze ona do Twojej metody, wyjaśniając, co inni zauważyli, że to błąd dzwoniącego nie twój.

Justsalt
źródło
tak, powinienem był to zrobić const string& s, właściwie zapomniałem. Ale czy mimo to czy nadal nie jestem podatny na niezdefiniowane zachowanie? Dzwoniący może nadal przekazać 0, prawda?
John Fitzpatrick,
2
Jeśli użyjesz odwołania non-const, program wywołujący nie będzie już mógł przejść 0, ponieważ obiekty tymczasowe nie będą dozwolone. Jednak korzystanie z biblioteki stałoby się znacznie bardziej irytujące (ponieważ obiekty tymczasowe nie będą dozwolone) i rezygnujesz z poprawności const.
Josh Kelley,
Kiedyś użyłem niepowiązanego parametru referencyjnego do klasy, w której musiałem móc go zapisać i dlatego zapewniłem, że nie nastąpi konwersja ani tymczasowe przekazanie.
CashCow
1

Co powiesz na przeciążenie bierze intparametr?

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

private:
    void myMethod(int);

Nie musisz nawet definiować przeciążenia. Próba połączenia myMethod(0)spowoduje błąd łącznika.

fredoverflow
źródło
1
To nie chroni przed kodem w pytaniu, gdzie 0ma char*typ.
Ben Voigt
0

Twoja metoda w pierwszym bloku kodu nigdy nie zostanie wywołana, jeśli spróbujesz wywołać ją za pomocą (char *)0. C ++ po prostu spróbuje utworzyć ciąg znaków i wygeneruje dla ciebie wyjątek. Próbowałeś?

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

Widzieć? Nie masz się czego obawiać.

Teraz, jeśli chcesz uchwycić to nieco bardziej wdzięcznie, po prostu nie powinieneś używać char *, wtedy problem się nie pojawi.

vy32
źródło
4
konstruowanie std :: string z zerowym wskaźnikiem będzie niezdefiniowanym zachowaniem
maniak ratchet
2
wyjątek EXC_BAD_ACCESS brzmi jak zerowa dereferencja, która spowoduje awarię twojego programu z awarią w dobry dzień
maniak zapadkowy
@ vy32 Część kodu, który piszę, który akceptuje, std::stringtrafia do bibliotek używanych w innych projektach, w których nie dzwonię. Szukam sposobu, aby z wdziękiem poradzić sobie z sytuacją i poinformować osobę dzwoniącą (być może z wyjątkiem), że przekazały niepoprawny argument bez awarii programu. (Oczywiście, osoba dzwoniąca może nie obsłużyć wyjątku, który zgłaszam, a program i tak się zawiesi.)
John Fitzpatrick,
2
@JohnFitzpatrick nie będą aable aby chronić siebie z NullPointer przeszła w std :: string, chyba że można przekonać comity standardy miały to być wyjątek zamiast niezdefiniowanej zachowanie
zapadkowy świra
@ratchetfreak W pewnym sensie myślę, że to była odpowiedź, której szukałem. Więc w zasadzie muszę się chronić.
John Fitzpatrick,
0

Jeśli martwisz się, że char * może być zerowymi wskaźnikami (np. Zwroty z zewnętrznych interfejsów API C), odpowiedzią jest użycie wersji const char * funkcji zamiast std :: string. to znaczy

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

Oczywiście potrzebujesz również wersji std :: string, jeśli chcesz zezwolić na ich użycie.

Zasadniczo najlepiej izolować wywołania zewnętrznych interfejsów API i argumentów marszałka na prawidłowe wartości.

Superfly Jon
źródło