Comment mieux protéger de 0 passé aux paramètres std :: string?

20

Je viens de réaliser quelque chose de dérangeant. Chaque fois que j'ai écrit une méthode qui accepte un std::stringcomme paramètre, je me suis ouvert à un comportement indéfini.

Par exemple, cela ...

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

... peut être appelé comme ça ...

char* s = 0;
myMethod(s);

... et je ne peux rien faire pour l'empêcher (à ma connaissance).

Ma question est donc: comment quelqu'un peut-il se défendre contre cela?

La seule approche qui me vient à l'esprit est de toujours écrire deux versions de toute méthode qui accepte un std::stringcomme paramètre, comme ceci:

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

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

Est-ce une solution courante et / ou acceptable?

EDIT: Certains ont souligné que je devrais accepter const std::string& sau lieu de std::string scomme paramètre. Je suis d'accord. J'ai modifié le post. Je ne pense pas que cela change la réponse.

John Fitzpatrick
la source
1
Hourra pour les abstractions qui fuient! Je ne suis pas un développeur C ++, mais y a-t-il une raison pour laquelle vous ne pouvez pas vérifier la c_strpropriété de l'objet chaîne ?
Mason Wheeler
6
assigner simplement 0 au constructeur char * est un comportement indéfini, donc c'est vraiment la faute des appelants
ratchet freak
4
@ratchetfreak Je ne savais pas que ce n'était pas char* s = 0défini. Je l'ai vu au moins quelques centaines de fois dans ma vie (généralement sous la forme de char* s = NULL). Avez-vous une référence pour appuyer cela?
John Fitzpatrick
3
Je voulais dire au std:string::string(char*)constructeur
ratchet freak
2
Je pense que votre solution est très bien, mais vous devriez envisager de ne rien faire du tout. :-) Votre méthode prend très clairement une chaîne, ne passe en aucun cas un pointeur nul lors de son appel une action valide - dans le cas où un appelant renverse accidentellement des valeurs nulles sur des méthodes comme celle-ci, plus tôt il explose dessus (plutôt que d'être signalé dans un fichier journal, par exemple) mieux c'est. S'il y avait un moyen d'empêcher quelque chose de ce genre lors de la compilation, alors vous devriez le faire, sinon je le laisserais. A MON HUMBLE AVIS. (BTW, êtes-vous sûr que vous ne pouviez pas prendre un const std::string&pour ce paramètre ...?)
Grimm The Opiner

Réponses:

21

Je ne pense pas que vous devriez vous protéger. C'est un comportement indéfini du côté de l'appelant. Ce n'est pas vous, c'est l'appelant qui appelle std::string::string(nullptr), ce qui n'est pas autorisé. Le compilateur permet de le compiler, mais il permet également de compiler d'autres comportements non définis.

La même manière serait d'obtenir une "référence nulle":

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

Celui qui déréférence le pointeur nul fait UB et en est responsable.

De plus, vous ne pouvez pas vous protéger après qu'un comportement indéfini s'est produit, car l'optimiseur a tout à fait le droit de supposer que le comportement indéfini ne s'est jamais produit, de sorte que la vérification si c_str()est nul peut être optimisée.

Vlad
la source
Il y a un commentaire magnifiquement écrit qui dit à peu près la même chose, vous devez donc avoir raison. ;-)
Grimm The Opiner
2
L'expression ici est protéger contre Murphy, pas Machiavel. Un programmeur malveillant bien versé peut trouver des moyens d'envoyer des objets mal formés pour même créer des références nulles s'il essaie assez fort, mais c'est son propre travail et vous pourriez aussi bien les laisser se tirer une balle dans le pied s'ils le veulent vraiment. Le mieux que l'on puisse faire est de prévenir les erreurs accidentelles. Dans ce cas, il est en effet rare que quelqu'un passe accidentellement un caractère * s = 0; à une fonction qui demande une chaîne bien formée.
YoungJohn
2

Le code ci-dessous donne un échec de compilation pour un passage explicite de 0, et un échec d'exécution pour un char*avec une valeur 0.

Notez que je n'implique pas que l'on devrait normalement faire cela, mais il ne fait aucun doute qu'il peut y avoir des cas où la protection contre l'erreur de l'appelant est justifiée.

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
la source
1

J'ai également rencontré ce problème il y a quelques années et je l'ai trouvé très effrayant. Cela peut arriver en passant un nullptrou en passant accidentellement un int avec la valeur 0. C'est vraiment absurde:

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

Cependant, à la fin, cela ne m'a dérangé que quelques fois. Et chaque fois, cela provoquait un crash immédiat lors du test de mon code. Aucune session nocturne ne sera donc nécessaire pour le réparer.

Je pense que surcharger la fonction avec const char*est une bonne idée.

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

Je souhaite qu'une meilleure solution soit possible. Mais il n'y en a pas.

StackedCrooked
la source
2
mais vous obtenez toujours une erreur d'exécution lorsque pour foo(0)et une erreur de compilation pourfoo(1)
Bryan Chen
1

Que diriez-vous de changer la signature de votre méthode pour:

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

De cette façon, l'appelant doit créer une chaîne avant de l'appeler, et la négligence qui vous inquiète causera des problèmes avant d'arriver à votre méthode, ce qui rend évident, ce que les autres ont souligné, que c'est l'erreur de l'appelant et non la vôtre.

Justsalt
la source
oui j'aurais dû y arriver const string& s, j'oubliais en fait. Mais même ainsi, ne suis-je pas encore vulnérable à un comportement indéfini? L'appelant peut toujours passer un 0, non?
John Fitzpatrick
2
Si vous utilisez une référence non constante, l'appelant ne peut plus passer 0, car les objets temporaires ne seront pas autorisés. Cependant, l'utilisation de votre bibliothèque deviendrait beaucoup plus ennuyeuse (car les objets temporaires ne seront pas autorisés) et vous renoncez à l'exactitude des const.
Josh Kelley
J'ai utilisé une fois un paramètre de référence non const à une classe où je devais être capable de le stocker et je me suis donc assuré qu'il n'y avait pas de conversion ou de passage temporaire.
CashCow
1

Que diriez-vous de fournir en surcharge le prend un intparamètre?

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

private:
    void myMethod(int);

Vous n'avez même pas besoin de définir la surcharge. Essayer d'appeler myMethod(0)déclenchera une erreur de l'éditeur de liens.

fredoverflow
la source
1
Cela ne protège pas contre le code dans la question, où le 0a un char*type.
Ben Voigt
0

Votre méthode dans le premier bloc de code ne sera jamais appelée si vous essayez de l'appeler avec un (char *)0. C ++ essaiera simplement de créer une chaîne et lèvera l'exception pour vous. L'avez-vous essayé?

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

Voir? Vous n'avez rien à craindre.

Maintenant, si vous voulez saisir cela un peu plus gracieusement, vous ne devriez tout simplement pas utiliser char *, le problème ne se posera pas.

vy32
la source
4
la construction d'une chaîne std :: avec un pointeur nul sera un comportement indéfini
ratchet freak
2
l'exception EXC_BAD_ACCESS sonne comme une déréférence nulle qui plantera votre programme avec une erreur de segmentation un bon jour
ratchet freak
@ vy32 Une partie du code que j'écris qui accepte std::stringva dans les bibliothèques utilisées par d'autres projets où je ne suis pas l'appelant. Je cherche un moyen de gérer la situation avec élégance et d'informer l'appelant (peut-être avec une exception) qu'il a passé un argument incorrect sans planter le programme. (OK, d'accord, l'appelant peut ne pas gérer une exception que je lance et le programme se bloquera de toute façon.)
John Fitzpatrick
2
@JohnFitzpatrick vous n'allez pas pouvoir vous protéger d'un nullpointer passé dans std :: string à moins que vous ne puissiez convaincre le comité de normalisation d'en faire une exception au lieu d'un comportement indéfini
ratchet freak
@ratchetfreak D'une certaine manière, je pense que c'est la réponse que je cherchais. Donc, fondamentalement, je dois me protéger.
John Fitzpatrick
0

Si vous craignez que char * soit des pointeurs potentiellement nuls (par exemple, les retours des API C externes), la réponse est d'utiliser une version const char * de la fonction au lieu de la std :: string. c'est à dire

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

Bien sûr, vous aurez également besoin de la version std :: string si vous souhaitez autoriser leur utilisation.

En général, il est préférable d'isoler les appels aux API externes et de rassembler les arguments en valeurs valides.

Superfly Jon
la source