Как лучше всего защитить от 0 передаваемых в std :: string параметров?

20

Я только что понял, что-то беспокоит. Каждый раз, когда я писал метод, который принимает a std::stringв качестве параметра, я открывал себя для неопределенного поведения.

Например, это ...

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

... можно назвать так ...

char* s = 0;
myMethod(s);

... и я ничего не могу сделать, чтобы предотвратить это (о чем я знаю).

Итак, мой вопрос: как кто-то защищает себя от этого?

Единственный подход, который приходит на ум, - это всегда писать две версии любого метода, который принимает std::stringв качестве параметра, например:

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

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

Это общее и / или приемлемое решение?

РЕДАКТИРОВАТЬ: Некоторые указали, что я должен принять, const std::string& sа не std::string sв качестве параметра. Я согласен. Я изменил пост. Я не думаю, что это меняет ответ, хотя.

Джон Фицпатрик
источник
1
Ура за дырявые абстракции! Я не разработчик C ++, но есть ли причина, по которой вы не можете проверить c_strсвойство строкового объекта ?
Мейсон Уилер
6
просто присвоение 0 в конструкторе char * - неопределенное поведение, так что это действительно ошибка вызывающей стороны
ratchet freak
4
@ratchetfreak Я не знал, что char* s = 0это не определено. Я видел это по крайней мере несколько сотен раз в своей жизни (обычно в форме char* s = NULL). У вас есть ссылка, чтобы подтвердить это?
Джон Фицпатрик
3
Я имел в виду std:string::string(char*)конструктор
трещотка урод
2
Я думаю, что ваше решение в порядке, но вы должны подумать о том, чтобы вообще ничего не делать. :-) Ваш метод довольно четко принимает строку, никоим образом не передает нулевой указатель при вызове его действительного действия - в случае, если вызывающий объект случайно выдал пустые значения методам, подобным этому, чем раньше он взорвется ( чем сообщать в журнале, например), тем лучше. Если бы был какой-то способ предотвратить это во время компиляции, то вы должны это сделать, иначе я бы оставил это. ПО МОЕМУ МНЕНИЮ. (Кстати, вы уверены, что не могли принять const std::string&для этого параметра ...?)
Grimm The Opiner

Ответы:

21

Я не думаю, что вы должны защищать себя. Это неопределенное поведение на стороне звонящего. Звонит не ты, а тот, кто не звонит std::string::string(nullptr). Компилятор позволяет его компилировать, но он также позволяет компилировать и другие неопределенные варианты поведения.

Точно так же можно получить «нулевую ссылку»:

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

Тот, кто разыменовывает нулевой указатель, создает UB и отвечает за него.

Более того, вы не можете защитить себя после того, как произошло неопределенное поведение, потому что оптимизатор имеет полное право предполагать, что неопределенное поведение никогда не происходило, поэтому проверки, c_str()равные нулю, могут быть оптимизированы.

Влад
источник
Есть красиво написанный комментарий, в котором говорится примерно то же самое, поэтому вы должны быть правы. ;-)
Grimm The Opiner
2
Фраза здесь защищает от Мерфи, а не Макиавелли. Хорошо осведомленный злонамеренный программист может найти способы отправки плохо сформированных объектов, чтобы даже создавать нулевые ссылки, если они стараются изо всех сил, но это их собственное дело, и вы могли бы также позволить им выстрелить себе в ногу, если они действительно этого хотят. Лучшее, что вы можете сделать, это предотвратить случайные ошибки. В этом случае действительно редко кто-то случайно передает символ char * s = 0; к функции, которая запрашивает правильно сформированную строку.
YoungJohn
2

Приведенный ниже код дает ошибку компиляции для явного прохода 0и ошибку времени выполнения для char*значения with 0.

Обратите внимание, что я не имею в виду, что обычно это следует делать, но, без сомнения, могут быть случаи, когда защита от ошибки вызывающего абонента оправдана.

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

Я также столкнулся с этой проблемой несколько лет назад, и я обнаружил, что это действительно очень страшная вещь. Это может произойти, если передать a nullptrили случайно передать int со значением 0. Это действительно абсурдно:

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

Тем не менее, в конце концов это всего лишь пару раз. И каждый раз это вызывало немедленный сбой при тестировании моего кода. Так что никаких ночных сессий не потребуется, чтобы это исправить.

Я думаю, что перегрузка функции const char*- это хорошая идея.

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

Хотелось бы, чтобы было лучше решение. Тем не менее, нет.

StackedCrooked
источник
2
но вы все равно получаете ошибку времени выполнения, когда foo(0)и ошибка компиляции дляfoo(1)
Брайан Чен
1

Как насчет изменения подписи вашего метода на:

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

Таким образом, вызывающий объект должен создать строку перед тем, как ее вызвать, и небрежность, о которой вы беспокоитесь, вызовет проблемы, прежде чем он попадет в ваш метод, делая очевидным то, на что указали другие, что это ошибка вызывающего, а не ваша.

Justsalt
источник
да, я должен был сделать это const string& s, я забыл на самом деле. Но даже так, разве я все еще не уязвим для неопределенного поведения? Абонент все еще может передать 0, верно?
Джон Фицпатрик
2
Если вы используете неконстантную ссылку, вызывающая сторона больше не может передавать 0, потому что временные объекты не будут разрешены. Однако, использование вашей библиотеки станет намного более раздражающим (потому что временные объекты не будут разрешены), и вы разочаровываетесь в правильности const.
Джош Келли
Однажды я использовал неконстантный ссылочный параметр для класса, где я должен был иметь возможность хранить его, и поэтому гарантировал, что не было ни преобразования, ни временного
ввода
1

Как насчет того, чтобы обеспечить перегрузку принимает intпараметр?

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

private:
    void myMethod(int);

Вам даже не нужно определять перегрузку. Попытка вызова myMethod(0)вызовет ошибку компоновщика.

fredoverflow
источник
1
Это не защищает от кода в вопросе, где 0имеет char*тип.
Бен Фойгт
0

Ваш метод в первом блоке кода никогда не будет вызван, если вы попытаетесь вызвать его с помощью (char *)0. C ++ просто попытается создать строку и выдаст исключение для вас. Вы пробовали это?

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

Видеть? Вам не о чем беспокоиться.

Теперь, если вы хотите поймать это более изящно, просто не стоит использовать char *, тогда проблема не возникнет.

vy32
источник
4
построение std :: string с нулевым указателем будет неопределенным поведением
трещотка урод
2
исключение EXC_BAD_ACCESS звучит как нулевая разыменование, которое приведет к аварийному завершению вашей программы с ошибкой в ​​хороший день
трещотка урод
@ vy32 Часть кода, который я пишу, который принимает, std::stringидет в библиотеки, используемые другими проектами, где я не вызывающий. Я ищу способ изящно справиться с ситуацией и сообщить звонящим (возможно, с исключением), что они передали неверный аргумент без сбоя программы. (Да, конечно, вызывающая сторона может не обработать исключение, которое я выбрасываю, и программа все равно будет аварийно завершена.)
Джон Фитцпатрик
2
@JohnFitzpatrick Вы не сможете защитить себя от нулевого указателя, переданного в std :: string, если только вы не сможете убедить стандарты в том, что это должно быть исключением, а не неопределенным поведением
ratchet freak
@ratchetfreak В некотором смысле я думаю, что это ответ, который я искал. Так что в основном я должен защитить себя.
Джон Фицпатрик
0

Если вы беспокоитесь о том, что char * может быть нулевым указателем (например, при возврате из внешних API C), ответом будет использование версии функции const char * вместо std :: string. т.е.

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

Конечно, вам также потребуется версия std :: string, если вы хотите разрешить их использование.

В общем, лучше всего изолировать вызовы внешних API и маршальные аргументы в допустимые значения.

Superfly Jon
источник