Làm thế nào để bảo vệ tốt nhất từ ​​0 được chuyển đến tham số std :: string?


20

Tôi vừa nhận ra một điều đáng lo ngại. Mỗi lần tôi viết một phương thức chấp nhận std::stringnhư một tham số, tôi đã tự mở ra hành vi không xác định.

Ví dụ, điều này ...

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

... có thể được gọi như thế này ...

char* s = 0;
myMethod(s);

... Và tôi không thể làm gì để ngăn chặn điều đó (mà tôi biết).

Vì vậy, câu hỏi của tôi là: Làm thế nào một người bảo vệ chính họ khỏi điều này?

Cách tiếp cận duy nhất nảy ra trong đầu là luôn viết hai phiên bản của bất kỳ phương thức nào chấp nhận std::stringmột tham số, như thế này:

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

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

Đây có phải là một giải pháp phổ biến và / hoặc chấp nhận được?

EDIT: Một số người đã chỉ ra rằng tôi nên chấp nhận const std::string& sthay vì std::string snhư một tham số. Tôi đồng ý. Tôi đã sửa đổi bài viết. Tôi không nghĩ rằng thay đổi câu trả lời mặc dù.


1
Hoan hô cho trừu tượng rò rỉ! Tôi không phải là nhà phát triển C ++, nhưng có lý do nào khiến bạn không thể kiểm tra thuộc c_strtính của đối tượng chuỗi không?
Mason Wheeler

6
chỉ cần gán 0 vào hàm tạo char * là hành vi không xác định, do đó, đó thực sự là lỗi của người gọi
ratchet freak

4
@ratchetfreak Tôi không biết điều đó char* s = 0là không xác định. Tôi đã nhìn thấy nó ít nhất vài trăm lần trong đời (thường ở dạng char* s = NULL). Bạn có một tài liệu tham khảo để sao lưu đó?
John Fitzpatrick

3
Tôi có nghĩa là với các nhà std:string::string(char*)xây dựng
ratchet freak

2
Tôi nghĩ rằng giải pháp của bạn là tốt, nhưng bạn nên xem xét không làm gì cả. :-) Phương thức của bạn khá rõ ràng có một chuỗi, không có cách nào truyền được một con trỏ null khi gọi nó là một hành động hợp lệ - trong trường hợp một người gọi vô tình làm hỏng null về các phương thức như thế này thì nó sẽ sớm thổi vào chúng (đúng hơn là hơn là được báo cáo trong một tệp nhật ký, ví dụ) thì tốt hơn. Nếu có một cách để ngăn chặn điều này vào thời gian biên dịch thì bạn nên làm điều đó, nếu không tôi sẽ bỏ nó. IMHO. (BTW, bạn có chắc là bạn không thể lấy const std::string&tham số đó không ...?)
Grimm The Opiner 9/12/13

Câu trả lời:


21

Tôi không nghĩ bạn nên tự bảo vệ mình. Đó là hành vi không xác định về phía người gọi. Không phải bạn, đó là người gọi std::string::string(nullptr), đó là những gì không được phép. Trình biên dịch cho phép nó được biên dịch, nhưng nó cũng cho phép các hành vi không xác định khác được biên dịch.

Cách tương tự sẽ nhận được "tham chiếu null":

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

Người đã hủy bỏ con trỏ null đang tạo ra UB và chịu trách nhiệm về nó.

Hơn nữa, bạn không thể tự bảo vệ mình sau khi hành vi không xác định đã xảy ra, bởi vì trình tối ưu hóa hoàn toàn có quyền cho rằng hành vi không xác định chưa bao giờ xảy ra, do đó, việc kiểm tra nếu c_str()null có thể được tối ưu hóa.


Có một bình luận đẹp bằng văn bản nói nhiều điều tương tự, vì vậy bạn phải đúng. ;-)
Grimm The Opiner

2
Cụm từ ở đây là bảo vệ chống lại Murphy, không phải Machiavelli. Một lập trình viên độc hại thông thạo có thể tìm cách gửi các đối tượng xấu để thậm chí tạo các tài liệu tham khảo null nếu họ cố gắng hết sức, nhưng đó là việc riêng của họ và bạn cũng có thể để họ tự bắn vào chân mình nếu họ thực sự muốn. Điều tốt nhất bạn có thể mong đợi làm là ngăn ngừa các lỗi vô ý. Trong trường hợp này, thực sự hiếm khi ai đó vô tình vượt qua trong char * s = 0; đến một hàm yêu cầu một chuỗi được hình thành tốt.
YoungJohn

2

Mã dưới đây cung cấp một lỗi biên dịch cho một lần vượt qua rõ ràng 0và một lỗi thời gian chạy cho một char*giá trị 0.

Lưu ý rằng tôi không ngụ ý rằng người ta thường nên làm điều này, nhưng không nghi ngờ gì có thể có trường hợp bảo vệ khỏi lỗi người gọi là hợp lý.

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

Tôi cũng gặp phải vấn đề này vài năm trước và tôi thấy nó thực sự là một điều rất đáng sợ. Nó có thể xảy ra bằng cách chuyển một nullptrhoặc vô tình chuyển một int có giá trị 0. Điều đó thực sự vô lý:

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

Tuy nhiên, cuối cùng điều này chỉ làm tôi khó chịu một vài lần. Và mỗi lần nó gây ra sự cố ngay lập tức khi kiểm tra mã của tôi. Vì vậy, không có phiên dài đêm sẽ được yêu cầu để sửa nó.

Tôi nghĩ rằng quá tải chức năng với const char*là một ý tưởng tốt.

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

Tôi muốn một giải pháp tốt hơn là có thể. Tuy nhiên, không có.


2
nhưng bạn vẫn gặp lỗi thời gian chạy khi for foo(0)và biên dịch lỗi chofoo(1)
Bryan Chen

1

Làm thế nào về việc thay đổi chữ ký của phương thức của bạn thành:

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

Bằng cách đó, người gọi phải tạo ra một chuỗi trước khi họ gọi nó và sự chậm chạp mà bạn lo lắng sẽ gây ra vấn đề trước khi nó đến với phương thức của bạn, làm cho rõ ràng, đó là lỗi của người gọi không phải của bạn.


vâng tôi nên làm nó const string& s, tôi thực sự quên. Nhưng ngay cả như vậy, không phải tôi vẫn dễ bị hành vi không xác định sao? Người gọi vẫn có thể vượt qua a 0, phải không?
John Fitzpatrick

2
Nếu bạn sử dụng tham chiếu không phải là const, thì người gọi không thể vượt qua 0 nữa, vì các đối tượng tạm thời sẽ không được phép. Tuy nhiên, việc sử dụng thư viện của bạn sẽ trở nên khó chịu hơn nhiều (vì các đối tượng tạm thời sẽ không được phép) và bạn đang từ bỏ tính chính xác.
Josh Kelley

Tôi đã từng sử dụng một tham số không phải là hằng số cho một lớp nơi tôi phải có thể lưu trữ nó và đảm bảo rằng không có chuyển đổi hoặc tạm thời được chuyển vào.
CashCow

1

Làm thế nào về bạn cung cấp khi quá tải mất một inttham số?

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

private:
    void myMethod(int);

Bạn thậm chí không phải xác định quá tải. Cố gắng gọi myMethod(0)sẽ gây ra lỗi liên kết.


1
Điều này không bảo vệ chống lại mã trong câu hỏi, nơi 0có một char*loại.
Ben Voigt

0

Phương thức của bạn trong khối mã đầu tiên sẽ không bao giờ được gọi nếu bạn cố gắng gọi nó bằng a (char *)0. C ++ sẽ chỉ đơn giản là cố gắng tạo một chuỗi và nó sẽ ném ngoại lệ cho bạn. Bạn đã thử à?

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

Xem? Bạn không có gì phải lo lắng.

Bây giờ nếu bạn muốn nắm bắt điều này một cách duyên dáng hơn một chút, đơn giản là bạn không nên sử dụng char *, thì vấn đề sẽ không phát sinh.


4
xây dựng chuỗi std :: với nullpulum sẽ là hành vi không xác định
ratchet freak

2
ngoại lệ EXC_BAD_ACCESS nghe có vẻ như là một sự vô hiệu hóa sẽ làm hỏng chương trình của bạn với một segfault vào một ngày tốt lành
ratchet freak

@ vy32 Một số mã tôi viết chấp nhận std::stringđi vào các thư viện được sử dụng bởi các dự án khác mà tôi không phải là người gọi. Tôi đang tìm cách để xử lý tình huống một cách duyên dáng và thông báo cho người gọi (có thể có ngoại lệ) rằng họ đã thông qua một đối số không chính xác mà không làm hỏng chương trình. (OK, được cho phép, người gọi có thể không xử lý ngoại lệ tôi ném và chương trình sẽ bị sập.)
John Fitzpatrick

2
@JohnFitzpatrick bạn sẽ không được aable để bảo vệ mình khỏi một nullpointer truyền vào std :: string, trừ khi bạn có thể thuyết phục các tiêu chuẩn tao nha để có nó là một ngoại lệ thay vì không xác định hành vi
quái ratchet

@ratchetfreak Theo cách tôi nghĩ đó là câu trả lời tôi đang tìm kiếm. Vì vậy, về cơ bản tôi phải tự bảo vệ mình.
John Fitzpatrick

0

Nếu bạn lo lắng về việc char * là con trỏ null tiềm năng (ví dụ trả về từ API C bên ngoài), câu trả lời là sử dụng phiên bản const char * của hàm thay vì std :: chuỗi một. I E

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

Tất nhiên bạn cũng sẽ cần phiên bản std :: string nếu bạn muốn cho phép chúng được sử dụng.

Nói chung, tốt nhất là cách ly các lệnh gọi với các API bên ngoài và các đối số sắp xếp thành các giá trị hợp lệ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.