¿Cómo proteger mejor del 0 pasado a los parámetros std :: string?


20

Me acabo de dar cuenta de algo inquietante. Cada vez que escribo un método que acepta std::stringun paramater, me abro a un comportamiento indefinido.

Por ejemplo, esto ...

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

... se puede llamar así ...

char* s = 0;
myMethod(s);

... y no hay nada que pueda hacer para evitarlo (que yo sepa).

Entonces mi pregunta es: ¿cómo se defiende alguien de esto?

El único enfoque que viene a la mente es escribir siempre dos versiones de cualquier método que acepte un std::stringparámetro, como este:

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

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

¿Es esta una solución común y / o aceptable?

EDITAR: Algunos han señalado que debería aceptar en const std::string& slugar de std::string scomo un parámetro. Estoy de acuerdo. Modifiqué la publicación. Sin embargo, no creo que eso cambie la respuesta.


1
¡Hurra por las abstracciones con fugas! No soy un desarrollador de C ++, pero ¿hay alguna razón por la que no pueda verificar la c_strpropiedad del objeto de cadena ?
Mason Wheeler

66
solo asignar 0 al constructor char * es un comportamiento indefinido, por lo que realmente es culpa de las personas que llaman
ratchet freak

44
@ratchetfreak No sabía que eso char* s = 0no está definido. Lo he visto al menos unos cientos de veces en mi vida (generalmente en forma de char* s = NULL). ¿Tiene una referencia para respaldar eso?
John Fitzpatrick

3
Me refería al std:string::string(char*)constructor
fanático del trinquete

2
Creo que su solución está bien, pero debería considerar no hacer nada en absoluto. :-) Su método claramente toma una cadena, de ninguna manera pasa un puntero nulo cuando lo llama una acción válida; en el caso de que una persona que llama accidentalmente esté tachando nulos sobre métodos como este, antes explota en ellos (más bien que ser informado en un archivo de registro, por ejemplo), mejor. Si hubiera una manera de evitar algo así en tiempo de compilación, entonces debería hacerlo, de lo contrario lo dejaría. EN MI HUMILDE OPINIÓN. (Por cierto, ¿estás seguro de que no podrías estar tomando un const std::string&para ese parámetro ...?)
Grimm The Opiner

Respuestas:


21

No creo que debas protegerte. Es un comportamiento indefinido del lado de la persona que llama. No eres tú, es la persona que llama std::string::string(nullptr), que es lo que no está permitido. El compilador permite que se compile, pero también permite que se compilen otros comportamientos indefinidos.

La misma forma sería obtener "referencia nula":

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

El que desreferencia el puntero nulo está haciendo UB y es responsable de ello.

Además, no puede protegerse después de que haya ocurrido un comportamiento indefinido, porque el optimizador tiene todo el derecho de asumir que el comportamiento indefinido nunca había sucedido, por lo que las comprobaciones si c_str()son nulas pueden optimizarse.


Hay un comentario bellamente escrito que dice casi lo mismo, por lo que debe tener razón. ;-)
Grimm The Opiner

2
La frase aquí es proteger contra Murphy, no contra Maquiavelo. Un programador malicioso bien versado puede encontrar formas de enviar objetos mal formados para incluso crear referencias nulas si se esfuerzan lo suficiente, pero eso es cosa suya y también puede dejar que se disparen en el pie si realmente lo desean. Lo mejor que puede hacer es evitar errores accidentales. En este caso, es raro que alguien pase accidentalmente un char * s = 0; a una función que pide una cadena bien formada.
YoungJohn

2

El siguiente código proporciona un error de compilación para un pase explícito 0y un error en tiempo de ejecución para un char*valor con 0.

Tenga en cuenta que no implica que normalmente deba hacerlo, pero sin duda puede haber casos en los que se justifique la protección contra el error de la persona que llama.

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

También me encontré con este problema hace unos años y, de hecho, me pareció muy aterrador. Puede suceder pasando un nullptro pasando accidentalmente un int con valor 0. Es realmente absurdo:

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

Sin embargo, al final esto solo me molestó un par de veces. Y cada vez causó un bloqueo inmediato al probar mi código. Por lo tanto, no se requerirán sesiones nocturnas para solucionarlo.

Creo que sobrecargar la función const char*es una buena 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));
}

Desearía que fuera posible una mejor solución. Sin embargo, no lo hay.


2
pero aún obtiene un error de tiempo de ejecución cuando for foo(0)y un error de compilación parafoo(1)
Bryan Chen

1

¿Qué tal cambiar la firma de su método a:

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

De esa manera, la persona que llama debe crear una cadena antes de llamarla, y la descuido que le preocupa causará problemas antes de que llegue a su método, lo que hace obvio, lo que otros han señalado, que es el error de la persona que llama, no el suyo.


Sí, debería haberlo hecho const string& s, lo olvidé en realidad. Pero aun así, ¿no sigo siendo vulnerable a un comportamiento indefinido? La persona que llama aún puede pasar un 0, ¿verdad?
John Fitzpatrick

2
Si usa una referencia no constante, la persona que llama ya no puede pasar 0, porque no se permitirán objetos temporales. Sin embargo, usar su biblioteca sería mucho más molesto (porque no se permitirán objetos temporales) y está renunciando a la corrección constante.
Josh Kelley

Una vez utilizado un parámetro de referencia no const a una clase donde tenía que ser capaz de almacenar y por lo tanto asegurado que no hubo conversión o ser temporal aprobada en.
Cashcow

1

¿Qué tal si proporciona sobrecarga la toma un intparámetro?

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

private:
    void myMethod(int);

Ni siquiera tiene que definir la sobrecarga. Intentar llamar myMethod(0)provocará un error de enlazador.


1
Esto no protege contra el código en la pregunta, donde 0tiene un char*tipo.
Ben Voigt

0

Su método en el primer bloque de código nunca se llamará si intenta llamarlo con un (char *)0. C ++ simplemente intentará crear una cadena y arrojará la excepción por usted. ¿Lo intentaste?

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

¿Ver? No tienes nada de qué preocuparte.

Ahora, si desea captar esto con un poco más de gracia, simplemente no debería estar usando char *, entonces el problema no surgirá.


44
la construcción de un std :: string con un NullPointer será un comportamiento indefinido
monstruo de trinquete

2
los sonidos de excepción EXC_BAD_ACCESS como una falta de referencia nula, que se colgará su programa con un error de segmentación en un buen día
monstruo de trinquete

@ vy32 Parte del código que escribo que acepta std::stringva a las bibliotecas utilizadas por otros proyectos donde no soy la persona que llama. Estoy buscando una manera de manejar con gracia la situación e informar a la persona que llama (tal vez con una excepción) que pasó un argumento incorrecto sin bloquear el programa. (De acuerdo, claro, la persona que llama podría no manejar una excepción que lanzo y el programa se bloqueará de todos modos.)
John Fitzpatrick

2
@JohnFitzpatrick que no van a ser aable para protegerse de un NullPointer pasado a std :: string a menos que pueda convencer a la cortesía estándares tener que ser una excepción en lugar de un comportamiento indefinido
monstruo de trinquete

@ratchetfreak En cierto modo, creo que esa es la respuesta que estaba buscando. Así que básicamente tengo que protegerme.
John Fitzpatrick

0

Si le preocupa que char * sea punteros potencialmente nulos (p. Ej., Retornos de API C externas), la respuesta es usar una versión const char * de la función en lugar de std :: string one. es decir

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

Por supuesto, también necesitará la versión std :: string si desea permitir que se usen.

En general, es mejor aislar las llamadas a API externas y argumentos de mariscal en valores válidos.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.