Como proteger melhor de 0 passado aos parâmetros std :: string?


20

Acabei de perceber algo perturbador. Toda vez que escrevo um método que aceita a std::stringcomo parâmetro, eu me abro a um comportamento indefinido.

Por exemplo, isso ...

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

... pode ser chamado assim ...

char* s = 0;
myMethod(s);

... e não há nada que eu possa fazer para evitá-lo (que eu saiba).

Então, minha pergunta é: como alguém se defende disso?

A única abordagem que vem à mente é sempre escrever duas versões de qualquer método que aceite a std::stringcomo parâ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));
    }
}

Esta é uma solução comum e / ou aceitável?

Edição: Alguns salientaram que eu deveria aceitar em const std::string& svez de std::string scomo um parâmetro. Concordo. Eu modifiquei a postagem. Acho que isso não muda a resposta.


1
Viva para abstrações com vazamentos! Não sou desenvolvedor de C ++, mas existe algum motivo para você não poder verificar a c_strpropriedade do objeto string ?
Mason Wheeler

6
apenas atribuindo 0 para o char * construtor é um comportamento indefinido, por isso é o interlocutor culpa realmente
catraca aberração

4
@ratchetfreak Eu não sabia que isso char* s = 0é indefinido. Eu já vi isso pelo menos algumas centenas de vezes na minha vida (geralmente na forma de char* s = NULL). Você tem uma referência para fazer backup disso?
John Fitzpatrick

3
Eu quis dizer com o std:string::string(char*)construtor
catraca aberração

2
Acho que sua solução está boa, mas você deve considerar não fazer nada. :-) Seu método usa claramente uma string, de maneira alguma passa um ponteiro nulo ao chamá-la de uma ação válida - no caso em que um chamador acidentalmente liga nulos para métodos como esse, quanto mais cedo ele explode (em vez disso, do que sendo relatado em um arquivo de log, por exemplo), melhor. Se houvesse uma maneira de impedir algo assim em tempo de compilação, você deveria fazê-lo, caso contrário, eu o deixaria. NA MINHA HUMILDE OPINIÃO. (BTW, você tem certeza que não poderia tomar um const std::string&para esse parâmetro ...?)
Grimm O Opiner

Respostas:


21

Eu não acho que você deveria se proteger. É um comportamento indefinido do lado do chamador. Não é você, é quem chama std::string::string(nullptr), o que não é permitido. O compilador permite que ele seja compilado, mas também permite que outros comportamentos indefinidos sejam compilados.

Da mesma maneira seria obter "referência nula":

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

Quem desreferencia o ponteiro nulo está criando UB e é responsável por ele.

Além disso, você não pode se proteger após o comportamento indefinido, porque o otimizador tem todo o direito de assumir que o comportamento indefinido nunca aconteceu, portanto, a verificação de c_str()nulo pode ser otimizada.


Há um comentário bem escrito que diz a mesma coisa, então você deve estar certo. ;-)
Grimm The Opiner

2
A frase aqui é proteger contra Murphy, não Maquiavel. Um programador mal-intencionado bem versado pode encontrar maneiras de enviar objetos mal formados para criar referências nulas mesmo que se esforcem o suficiente, mas isso é tarefa deles e você também pode deixá-los dar um tiro no pé se realmente quiserem. O melhor que você pode fazer é evitar erros acidentais. Nesse caso, é raro que alguém passe acidentalmente um caractere * s = 0; para uma função que solicita uma string bem formada.
precisa

2

O código abaixo fornece uma falha de compilação para uma passagem explícita de 0e uma falha em tempo de execução para a char*with value 0.

Observe que eu não implica que alguém deva fazer isso normalmente, mas sem dúvida pode haver casos em que a proteção contra o erro de chamada é justificada.

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

Eu também encontrei esse problema há alguns anos e achei realmente muito assustador. Isso pode acontecer nullptrpassando um int ou acidentalmente passando um int com valor 0. É realmente um absurdo:

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

No entanto, no final, isso só me incomodou algumas vezes. E cada vez que causava uma falha imediata ao testar meu código. Portanto, não serão necessárias sessões noturnas para corrigi-lo.

Eu acho que sobrecarregar a função const char*é uma boa ideia.

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

Eu gostaria que uma solução melhor fosse possível. No entanto, não há.


2
mas você ainda recebe erro de tempo de execução quando foo(0)e erro de compilação parafoo(1)
Bryan Chen

1

Que tal alterar a assinatura do seu método para:

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

Dessa forma, o chamador precisa criar uma sequência antes de chamá-la, e a negligência com a qual você está preocupado causará problemas antes que ele atinja o seu método, tornando óbvio o que os outros apontaram, que é o erro do chamador e não o seu.


sim, eu deveria ter conseguido const string& s, na verdade eu esqueci. Mas mesmo assim, ainda não estou vulnerável a comportamentos indefinidos? O chamador ainda pode passar um 0, certo?
John Fitzpatrick

2
Se você usar uma referência que não seja const, o chamador não poderá mais passar 0, porque objetos temporários não serão permitidos. No entanto, o uso da sua biblioteca se tornaria muito mais irritante (porque objetos temporários não serão permitidos) e você estará desistindo da correção constante.
Josh Kelley

Uma vez eu usei um parâmetro de referência não-const a uma classe onde eu tinha que ser capaz de armazená-lo e assegurou, portanto, que não houve conversão ou ser temporária passada.
Cashcow

1

Que tal você fornecer sobre sobrecarga o leva um intparâmetro?

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

private:
    void myMethod(int);

Você nem precisa definir a sobrecarga. Tentar ligar myMethod(0)acionará um erro do vinculador.


1
Isso não protege contra o código da pergunta, onde 0tem um char*tipo.
Ben Voigt

0

Seu método no primeiro bloco de código nunca será chamado se você tentar chamá-lo com a (char *)0. O C ++ simplesmente tentará criar uma string e lançará a exceção para você. Você tentou?

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

Vejo? Você não tem nada com o que se preocupar.

Agora, se você quiser entender isso um pouco mais graciosamente, simplesmente não deve usá-lo char *, o problema não surgirá.


4
a construção de um std :: string com um nullpointer será um comportamento indefinido
aberração catraca

2
os sons de exceção EXC_BAD_ACCESS como um dereference nulo que irá travar o seu programa com um segfault em um bom dia
aberração catraca

@ vy32 Alguns dos códigos que escrevo que aceitam std::stringvão para bibliotecas usadas por outros projetos nos quais eu não sou o responsável pela chamada. Estou procurando uma maneira de lidar com a situação normalmente e informar ao chamador (talvez com uma exceção) que eles transmitiram um argumento incorreto sem travar o programa. (OK, concedido, o chamador pode não lidar com uma exceção eu jogo eo programa irá falhar de qualquer maneira.)
John Fitzpatrick

2
@JohnFitzpatrick você não vão ser aable para se proteger de um nullpointer passou para std :: string, a menos que você pode convencer o comity normas para tê-lo ser uma exceção em vez de indefinida comportamento
aberração catraca

@ratchetfreak De certa forma, acho que essa é a resposta que eu estava procurando. Então, basicamente, tenho que me proteger.
John Fitzpatrick

0

Se você está preocupado com char * sendo ponteiros potencialmente nulos (por exemplo, retornos de APIs externas C), a resposta é usar uma versão const char * da função em vez da std :: string one. ie

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

Claro que você também precisará da versão std :: string se quiser permitir que elas sejam usadas.

Em geral, é melhor isolar chamadas para APIs externas e argumentos de empacotamento em valores válidos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.