วิธีการป้องกันที่ดีที่สุดจาก 0 ผ่านไปยัง std :: พารามิเตอร์สตริง?


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

นี่เป็นวิธีแก้ปัญหาทั่วไปและ / หรือที่ยอมรับได้หรือไม่?

แก้ไข: บางคนชี้ให้เห็นว่าฉันควรยอมรับconst std::string& sแทนที่จะstd::string sเป็นพารามิเตอร์ ฉันเห็นด้วย. ฉันแก้ไขโพสต์ ฉันไม่คิดว่าจะเปลี่ยนคำตอบ


1
ไชโยสำหรับ abstractions รั่ว! ฉันไม่ใช่นักพัฒนา C ++ แต่มีเหตุผลใดที่คุณไม่สามารถตรวจสอบc_strคุณสมบัติของวัตถุสตริงได้
Mason Wheeler

6
เพียงแค่กำหนด 0 เข้าถ่าน * คอนสตรัคเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ดังนั้นจึงเป็นสายที่ผิดจริงๆ
วงล้อประหลาด

4
@ratchetfreak ฉันไม่ทราบว่าไม่ได้char* s = 0กำหนด ฉันเคยเห็นมันอย่างน้อยสองสามร้อยครั้งในชีวิตของฉัน (มักจะอยู่ในรูปแบบของchar* s = NULL) คุณมีข้อมูลอ้างอิงเพื่อสำรองข้อมูลหรือไม่?
John Fitzpatrick

3
ฉันหมายถึงผู้std:string::string(char*)สร้าง
วงล้อประหลาด

2
ฉันคิดว่าโซลูชันของคุณใช้ได้ดี แต่คุณควรพิจารณาที่จะไม่ทำอะไรเลย :-) วิธีการของคุณค่อนข้างชัดเจนใช้สตริงไม่มีทางที่จะผ่านตัวชี้โมฆะเมื่อเรียกมันว่าการกระทำที่ถูกต้อง - ในกรณีที่ผู้โทรกำลัง bunging nulls โดยไม่ตั้งใจเกี่ยวกับวิธีการเช่นนี้ในไม่ช้ามันก็ระเบิดพวกเขา ดีกว่าถูกรายงานไว้ในล็อกไฟล์ตัวอย่าง) ดีกว่า หากมีวิธีการป้องกันบางสิ่งบางอย่างในเวลารวบรวมคุณควรทำอย่างนั้นมิฉะนั้นฉันจะทิ้งมันไว้ IMHO (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()มีค่าเป็นโมฆะหรือไม่


มีความคิดเห็นที่เขียนอย่างสวยงามซึ่งพูดเหมือนกันมากดังนั้นคุณต้องพูดถูก ;-)
Grimm The Opiner

2
วลีที่นี่เป็นการป้องกันเมอร์ฟีไม่ใช่ Machiavelli โปรแกรมเมอร์ที่เป็นอันตรายรอบรู้สามารถหาวิธีส่งวัตถุที่มีรูปแบบไม่ดีเพื่อสร้างการอ้างอิงที่เป็นโมฆะหากพวกเขาพยายามอย่างหนักพอ แต่นั่นเป็นสิ่งที่พวกเขาทำเองและคุณอาจปล่อยให้พวกเขายิงตัวเองด้วยเท้า สิ่งที่ดีที่สุดที่คุณสามารถทำได้คือป้องกันข้อผิดพลาดโดยไม่ตั้งใจ ในกรณีนี้มันเป็นของหายากแน่นอนว่าบางคนอาจบังเอิญผ่าน 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

อย่างไรก็ตามในท้ายที่สุดนี้เพียงฉัน bugged สองสามครั้ง และทุกครั้งที่มันทำให้เกิดความผิดพลาดทันทีเมื่อทดสอบรหัสของฉัน ดังนั้นไม่จำเป็นต้องทำการแก้ไขในช่วงกลางคืน

ฉันคิดว่าการใช้งานฟังก์ชั่นมากเกินไป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)
Bryan Chen

1

วิธีเปลี่ยนลายเซ็นของวิธีการของคุณเป็น:

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

วิธีที่ผู้โทรต้องสร้างสตริงก่อนที่จะเรียกมันและความเลอะเทอะที่คุณกังวลจะทำให้เกิดปัญหาก่อนที่จะเข้าสู่วิธีการของคุณทำให้ชัดเจนสิ่งที่คนอื่น ๆ ชี้ให้เห็นว่าเป็นข้อผิดพลาดของผู้โทรไม่ใช่ของคุณ


ใช่ฉันควรจะทำมันconst string& sฉันลืมจริง แต่ถึงกระนั้นฉันก็ยังไม่เสี่ยงต่อพฤติกรรมที่ไม่ได้กำหนดหรือไม่ ผู้โทรยังสามารถผ่าน a 0ได้ใช่ไหม
John Fitzpatrick

2
หากคุณใช้การอ้างอิงที่ไม่ใช่ const ผู้โทรจะไม่สามารถผ่าน 0 ได้อีกต่อไปเพราะจะไม่อนุญาตให้ใช้วัตถุชั่วคราว อย่างไรก็ตามการใช้ห้องสมุดของคุณจะกลายเป็นเรื่องน่ารำคาญมากขึ้น (เพราะไม่อนุญาตให้ใช้วัตถุชั่วคราว) และคุณจะต้องแก้ไขให้ถูกต้อง
Josh Kelley

ฉันเคยใช้พารามิเตอร์อ้างอิงที่ไม่ใช่แบบ const กับคลาสที่ฉันต้องสามารถเก็บไว้และทำให้มั่นใจได้ว่าไม่มีการแปลงหรือการส่งผ่านชั่วคราว
CashCow

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

(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 ฟังดูเหมือนเป็นข้อผิดพลาดแบบ null ซึ่งจะทำให้โปรแกรมของคุณทำงานผิดพลาดพร้อมกับ segfault ในวันที่ดี
ratchet freak

@ vy32 บางรหัสที่ฉันเขียนที่ยอมรับstd::stringจะเข้าสู่ห้องสมุดที่ใช้โดยโครงการอื่นที่ฉันไม่ใช่ผู้โทร ฉันกำลังมองหาวิธีที่จะจัดการกับสถานการณ์อย่างสง่างามและแจ้งผู้โทร (อาจมีข้อยกเว้น) ว่าพวกเขาผ่านการโต้แย้งที่ไม่ถูกต้องโดยไม่ต้องหยุดโปรแกรม (ตกลงรับผู้โทรอาจไม่จัดการกับข้อยกเว้นที่ฉันโยนและโปรแกรมจะหยุดทำงานต่อไป)
John Fitzpatrick

2
@JohnFitzpatrick คุณจะไม่สามารถป้องกันตัวเองจาก nullpointer ที่ส่งไปยัง std :: string เว้นแต่คุณจะสามารถโน้มน้าวใจ comity มาตรฐานให้เป็นข้อยกเว้นแทนพฤติกรรมที่ไม่ได้กำหนด
ratchet freak

@ ratchetfreak ในแบบที่ฉันคิดว่านั่นเป็นคำตอบที่ฉันกำลังมองหา ดังนั้นโดยทั่วไปฉันต้องป้องกันตัวเอง
John Fitzpatrick

0

หากคุณกังวลว่าถ่าน * เป็นพอยน์เตอร์ที่อาจเป็นโมฆะ (เช่นผลตอบแทนจาก C API ภายนอก) คำตอบคือใช้ฟังก์ชัน const char * ของฟังก์ชันแทน std :: string one กล่าวคือ

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

แน่นอนว่าคุณจะต้องใช้ std :: string version เช่นกันหากคุณต้องการอนุญาตให้ใช้

โดยทั่วไปการแยกการเรียกไปยัง API ภายนอกและอาร์กิวเมนต์ marshal เป็นค่าที่ถูกต้อง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.