Як найкраще захистити від 0 переданих параметрів std :: string?


20

Я щойно зрозумів щось тривожне. Кожного разу, коли я писав метод, який приймає параметр 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));
    }
}

Це загальне та / або прийнятне рішення?

EDIT: Деякі вказували, що я повинен приймати const std::string& sзамість цього std::string sпараметра. Я згоден. Я змінив публікацію. Я не думаю, що це змінює відповідь.


1
Ура за нещільні абстракції! Я не розробник C ++, але чи є якась причина, чому ви не могли перевірити c_strвластивість об'єкта string ?
Мейсон Уілер

6
просто присвоєння 0 конструктору char * - це невизначена поведінка, тож справді вини абонентів
грохот freak

4
@ratchetfreak Я не знав, що char* s = 0це не визначено. Я бачив це принаймні кілька сотень разів у своєму житті (як правило, у формі char* s = NULL). Чи є у вас посилання на це?
Джон Фіцпатрік

3
Я мав на увазі для std:string::string(char*)конструктора
храповика, виродка

2
Я думаю, що ваше рішення чудове, але вам слід подумати про те, щоб взагалі нічого не робити. :-) Ваш метод досить чітко займає рядок, ні в якому разі не передаючи нульовий покажчик, називаючи його дійсною дією - у тому випадку, якщо абонент випадково змінює нулі щодо таких методів, чим швидше він підірветься на них (швидше чим, наприклад, повідомляється у файлі журналу), тим краще. Якщо був би спосіб запобігти цьому щось під час компіляції, тоді ви повинні це зробити, інакше я б це залишив. ІМХО. (BTW, ти впевнений, що ти не зміг взяти 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()це недійсне, можна оптимізувати.


Є прекрасно написаний коментар, який говорить майже те саме, тож ви повинні мати рацію. ;-)
Грімм Оглядач

2
Фраза тут - захист від Мерфі, а не Макіавеллі. Добре розбірливий шкідливий програміст може знайти способи надсилати недобре сформовані об’єкти навіть для створення нульових посилань, якщо вони постараються досить наполегливо, але це їх власна робота, і ви можете також дозволити їм стріляти в ногу, якщо вони дійсно хочуть. Найкраще, що можна очікувати, - це запобігти випадкові помилки. У цьому випадку справді рідко хтось випадково перейшов у знак char * s = 0; до функції, яка запитує добре сформований рядок.
YoungJohn

2

Нижче наведений код дає збій компіляції для явного проходу 0та збій у процесі виконання для char*значення зі значенням 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));
}

Бажаю, щоб приємніше рішення було можливим. Однак цього немає.


2
але ви все одно отримаєте помилку виконання під час foo(0)та помилки компіляції дляfoo(1)
Брайан Чен

1

Як щодо зміни підпису вашого методу на:

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

Таким чином, абонент повинен створити рядок, перш ніж вони зателефонують, і неохайність, про яку ви переживаєте, спричинить проблеми, перш ніж потрапити на ваш метод, зробивши очевидним, що вказали інші, що помилка абонента не ваша.


так, я мав би це зробити const string& s, я фактично забув. Але навіть невже я все ще не вразливий до невизначеної поведінки? Абонент може все-таки пройти а 0, правда?
Джон Фіцпатрік

2
Якщо ви використовуєте посилання non-const, то абонент більше не може передавати 0, оскільки тимчасові об'єкти заборонено. Однак використання вашої бібліотеки стане набагато прикровішим (оскільки тимчасові об’єкти заборонено), і ви відмовитеся від коректності const.
Джош Келлі

Одного разу я використав посилальний параметр non-const до класу, де я мав змогу його зберігати, і тому переконався, що не було перетворень чи тимчасових
передач

1

Як щодо ви забезпечуєте перевантаження параметра приймає int?

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

private:
    void myMethod(int);

Вам навіть не потрібно визначати перевантаження. Спроба зателефонувати myMethod(0)призведе до помилки зв’язку.


1
Це не захищає від коду у питанні, де 0має char*тип.
Ben Voigt

0

Ваш метод у першому кодовому блоці ніколи не буде викликаний, якщо ви спробуєте викликати його з a (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 *, тоді проблема не виникне.


4
побудова std :: string з nullpointer буде невизначеною поведінкою
ratchet freak

2
виняток EXC_BAD_ACCESS звучить як нульовий заниження, який розбить вашу програму із segfault у добрий день
грохот freak

@ vy32 Частина коду, який я пишу, який приймає, std::stringйде в бібліотеки, які використовуються в інших проектах, де я не викликаю. Я шукаю спосіб витончено впоратися з ситуацією та повідомити абонента (можливо, за винятком), що вони передали неправильний аргумент без збоїв у програмі. (Гаразд, надано, абонент може не обробляти виняток, який я кидаю, і програма все одно вийде з ладу.)
Джон Фіцпатрік

2
@JohnFitzpatrick, ви не збираєтеся захищати себе від нульового покажчика, що перейшов у std :: string, якщо ви не зможете переконати стандартність у тому, щоб це було винятком замість невизначеної поведінки
храповик урод

@ratchetfreak Я певною мірою думаю, що це відповідь, яку я шукав. Тому в основному я повинен захищати себе.
Джон Фіцпатрік

0

Якщо ви турбуєтесь про те, що char * є потенційно нульовими покажчиками (наприклад, поверненнями із зовнішніх API), відповідь полягає у використанні версії функції const char * замість std :: string one. тобто

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

Звичайно, вам також знадобиться версія std :: string, якщо ви хочете дозволити їх використовувати.

Взагалі найкраще ізолювати виклики до зовнішніх API та аргументацію маршалу у дійсні значення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.