Tại sao Clang / LLVM cảnh báo tôi về việc sử dụng mặc định trong câu lệnh chuyển đổi trong đó tất cả các trường hợp liệt kê được bảo hiểm?


34

Hãy xem xét enum và câu lệnh switch sau đây:

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

Tôi là một lập trình viên Objective-C, nhưng tôi đã viết nó bằng C thuần túy cho nhiều đối tượng hơn.

Clang / LLVM 4.1 với -Weverything cảnh báo tôi ở dòng mặc định:

Nhãn mặc định trong switch bao gồm tất cả các giá trị liệt kê

Bây giờ, tôi có thể sắp xếp xem tại sao nó lại ở đó: trong một thế giới hoàn hảo, các giá trị duy nhất nhập vào đối số theMasksẽ nằm trong enum, vì vậy không cần mặc định là cần thiết. Nhưng chuyện gì sẽ xảy ra nếu một số hack xuất hiện và ném một int chưa được khởi tạo vào chức năng tuyệt đẹp của tôi? Chức năng của tôi sẽ được cung cấp dưới dạng thư viện và tôi không kiểm soát được những gì có thể xảy ra ở đó. Sử dụng defaultlà một cách rất gọn gàng để xử lý này.

Tại sao các vị thần LLVM coi hành vi này không xứng đáng với thiết bị vô sinh của họ? Tôi có nên đi trước điều này bằng một câu lệnh if để kiểm tra đối số không?


1
Tôi nên nói rằng, lý do của tôi cho - Bất cứ điều gì là để làm cho mình trở thành một lập trình viên tốt hơn. Như NSHipster nói : "Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode.".
Swizzlr

2
-Weverythingcó thể hữu ích, nhưng hãy cẩn thận về việc làm thay đổi mã của bạn quá nhiều để xử lý nó. Một số trong những cảnh báo đó không chỉ vô giá trị mà còn phản tác dụng, và tốt nhất là tắt đi. (Thật vậy, đó là trường hợp sử dụng cho -Weverything: bắt đầu với nó và tắt những gì không có ý nghĩa.)
Steven Fisher

Bạn có thể mở rộng một chút về thông điệp cảnh báo cho hậu thế không? Họ thường có nhiều hơn thế.
Steven Fisher

Sau khi đọc câu trả lời, tôi vẫn thích giải pháp ban đầu của bạn. Sử dụng câu lệnh mặc định IMHO tốt hơn so với các lựa chọn thay thế được đưa ra trong các câu trả lời. Chỉ cần một lưu ý nhỏ: các câu trả lời thực sự tốt và nhiều thông tin, chúng cho thấy một cách tuyệt vời để giải quyết vấn đề.
bbaja42

@StevenFisher Đó là toàn bộ cảnh báo. Và như Killian đã chỉ ra rằng nếu tôi sửa đổi enum của mình sau này, nó sẽ mở ra khả năng các giá trị hợp lệ chuyển sang triển khai mặc định. Nó dường như là một mẫu thiết kế tốt (nếu nó là một thứ như vậy).
Swizzlr

Câu trả lời:


31

Đây là phiên bản không phải là vấn đề báo cáo của clang hay báo cáo mà bạn đang đề phòng:

void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}

Killian đã giải thích lý do tại sao tiếng kêu phát ra cảnh báo: nếu bạn mở rộng enum, bạn sẽ rơi vào trường hợp mặc định có thể không phải là điều bạn muốn. Điều chính xác cần làm là loại bỏ trường hợp mặc định và nhận cảnh báo cho các điều kiện chưa được xử lý .

Bây giờ bạn lo ngại rằng ai đó có thể gọi hàm của bạn với một giá trị nằm ngoài bảng liệt kê. Nghe có vẻ như không đáp ứng điều kiện tiên quyết của chức năng: nó được ghi nhận để mong đợi một giá trị từtestingMask bảng liệt kê nhưng lập trình viên đã thông qua một thứ khác. Vì vậy, làm cho lỗi lập trình viên sử dụng assert()(hoặc NSCAssert()như bạn đã nói bạn đang sử dụng Objective-C). Làm cho chương trình của bạn sụp đổ với một thông báo giải thích rằng lập trình viên đang làm sai, nếu lập trình viên làm sai.


6
+1. Là một sửa đổi nhỏ, tôi thích có từng trường hợp return;và thêm một assert(false);kết thúc (thay vì lặp lại chính mình bằng cách liệt kê các enum pháp lý trong một chữ cái đầu assert()và trong switch).
Josh Kelley

1
Điều gì sẽ xảy ra nếu enum không chính xác không được thông qua bởi một lập trình viên câm, mà là do lỗi hỏng bộ nhớ? Khi tài xế nhấn nút ngắt chiếc Toyota của họ và một lỗi đã làm hỏng enum, thì chương trình xử lý ngắt sẽ bị sập & ghi, và sẽ có một văn bản hiển thị cho người lái xe: "lập trình viên đang làm sai, lập trình viên tồi! ". Tôi hoàn toàn không thấy điều này sẽ giúp người dùng như thế nào, trước khi họ lái xe qua rìa của một vách đá.

2
@Lundin là những gì OP đang làm, hay đó là một trường hợp cạnh lý thuyết vô nghĩa mà bạn vừa xây dựng? Bất kể, "lỗi hỏng bộ nhớ" [i] là lỗi lập trình viên, [ii] không phải là thứ bạn có thể tiếp tục theo cách có ý nghĩa (ít nhất là không phải với các yêu cầu như đã nêu trong câu hỏi).

1
@GrahamLee Tôi chỉ nói rằng "nằm xuống và chết" không nhất thiết là lựa chọn tốt nhất khi bạn phát hiện ra một lỗi không mong muốn.

1
Nó có một vấn đề mới mà bạn có thể để cho khẳng định và các trường hợp không đồng bộ hóa.
dùng253751

9

Có một defaultnhãn ở đây là một chỉ số mà bạn bối rối về những gì bạn đang mong đợi. Vì bạn đã cạn kiệt tất cả có thể enumgiá trị một cách rõ ràng, những defaultkhông thể có thể được thực thi, và bạn không cần nó để bảo vệ chống lại tương lai thay đổi một trong hai, bởi vì nếu bạn mở rộng enum, sau đó các cấu trúc sẽ đã tạo ra một cảnh báo.

Vì vậy, trình biên dịch thông báo rằng bạn đã bao gồm tất cả các cơ sở nhưng dường như nghĩ rằng bạn không có, và đó luôn là một dấu hiệu xấu. Bằng cách nỗ lực tối thiểu để thay đổiswitch biểu mẫu dự kiến, bạn chứng minh cho trình biên dịch rằng những gì bạn dường như đang làm là những gì bạn đang thực sự làm và bạn biết điều đó.


1
Nhưng mối quan tâm của tôi là một biến bẩn và bảo vệ chống lại điều đó (như, như tôi đã nói, một int chưa được khởi tạo). Dường như với tôi rằng câu lệnh switch có thể được mặc định trong tình huống như vậy.
Swizzlr

Tôi không biết định nghĩa cho Objective-C, nhưng tôi cho rằng một kiểu chữ sẽ được trình biên dịch thực thi. Nói cách khác, một giá trị "chưa được khởi tạo" chỉ có thể nhập phương thức nếu chương trình của bạn đã thể hiện hành vi không xác định và tôi thấy hoàn toàn hợp lý khi trình biên dịch không tính đến điều đó.
Kilian Foth

3
@KilianFoth không, họ không. Enum Objective-C là enum C, không phải enum Java. Bất kỳ giá trị nào từ kiểu tích phân cơ bản thể xuất hiện trong đối số hàm.

2
Ngoài ra, enums có thể được thiết lập trong thời gian chạy, từ số nguyên. Vì vậy, chúng có thể chứa bất kỳ giá trị có thể tưởng tượng.

OP là chính xác. thử nghiệm mask; chức năng của tôi (m); rất có thể sẽ gặp trường hợp mặc định.
Matthew James Briggs

4

Clang bối rối, có một tuyên bố mặc định có thực hành hoàn toàn tốt, nó được gọi là lập trình phòng thủ và được coi là thực hành lập trình tốt (1). Nó được sử dụng nhiều trong các hệ thống quan trọng, mặc dù có lẽ không phải trong lập trình máy tính để bàn.

Mục đích của lập trình phòng thủ là bắt những lỗi không mong muốn mà trên lý thuyết sẽ không bao giờ xảy ra. Một lỗi không mong muốn như vậy không nhất thiết là lập trình viên cung cấp cho hàm một đầu vào không chính xác, hoặc thậm chí là một "hack ác". Nhiều khả năng, nó có thể được gây ra bởi một biến bị hỏng: tràn bộ đệm, tràn ngăn xếp, mã chạy trốn và các lỗi tương tự không liên quan đến chức năng của bạn có thể gây ra điều này. Và trong trường hợp các hệ thống nhúng, các biến có thể thay đổi do EMI, đặc biệt nếu bạn đang sử dụng các mạch RAM ngoài.

Đối với những gì viết bên trong câu lệnh mặc định ... nếu bạn nghi ngờ rằng chương trình đã bị hỏng khi bạn kết thúc ở đó, thì bạn cần một số cách xử lý lỗi. Trong nhiều trường hợp, bạn có thể chỉ cần thêm một tuyên bố trống rỗng với một bình luận: "bất ngờ nhưng không thành vấn đề", v.v., để cho thấy rằng bạn đã suy nghĩ về tình huống không thể xảy ra.


(1) MISRA-C: 2004 15.3.


1
Xin lưu ý rằng các lập trình viên PC trung bình thường thấy khái niệm lập trình phòng thủ hoàn toàn xa lạ, vì quan điểm của họ về một chương trình là một điều không tưởng trừu tượng trong đó không có gì có thể sai trong lý thuyết sẽ sai trong thực tế. Do đó, bạn sẽ nhận được câu trả lời khá đa dạng cho câu hỏi của bạn tùy thuộc vào người bạn hỏi.

3
Clang không nhầm lẫn; nó đã được yêu cầu rõ ràng để cung cấp tất cả các cảnh báo, bao gồm những cảnh báo không phải lúc nào cũng hữu ích, chẳng hạn như cảnh báo về phong cách. (Cá nhân tôi chọn tham gia cảnh báo này vì tôi không muốn mặc định trong các công tắc enum của mình; có chúng nghĩa là tôi không nhận được cảnh báo nếu tôi thêm giá trị enum và không xử lý nó. Vì lý do này, tôi luôn xử lý trường hợp giá trị xấu bên ngoài enum.)
Jens Ayton

2
@JensAyton Thật bối rối. Tất cả các công cụ phân tích tĩnh chuyên nghiệp cũng như tất cả các trình biên dịch tuân thủ MISRA sẽ đưa ra cảnh báo nếu câu lệnh chuyển đổi thiếu câu lệnh mặc định. Hãy thử một mình.

4
Mã hóa sang MISRA-C không phải là cách sử dụng hợp lệ duy nhất cho trình biên dịch C và một lần nữa, -Weverythingbật mọi cảnh báo, không phải là lựa chọn phù hợp với trường hợp sử dụng cụ thể. Không phù hợp với khẩu vị của bạn không giống như nhầm lẫn.
Jens Ayton

2
@Lundin: Tôi khá chắc chắn rằng việc gắn bó với MISRA-C không làm cho các chương trình trở nên an toàn và không có lỗi ... và tôi cũng chắc chắn rằng có rất nhiều mã C an toàn, không có lỗi từ những người thậm chí chưa từng nghe nói về MISRA. Trên thực tế, nó ít hơn nhiều so với ý kiến ​​của một ai đó (phổ biến, nhưng không thể tranh cãi), và có những người tìm thấy một số quy tắc của nó tùy ý và đôi khi thậm chí có hại bên ngoài bối cảnh mà chúng được thiết kế.
cHao

4

Vẫn tốt hơn:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

Điều này ít xảy ra lỗi khi thêm các mục vào enum. Bạn có thể bỏ qua kiểm tra cho> = 0 nếu bạn đặt giá trị enum của mình không dấu. Phương pháp này chỉ hoạt động nếu bạn không có khoảng trống trong các giá trị enum của mình, nhưng đó thường là trường hợp.


2
Điều này đang gây ô nhiễm loại với các giá trị mà bạn không muốn loại đó chứa. Nó có điểm tương đồng với WTF cũ tốt ở đây: thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto

4

Nhưng chuyện gì sẽ xảy ra nếu một số hack xuất hiện và ném một int chưa được khởi tạo vào chức năng tuyệt đẹp của tôi?

Sau đó, bạn nhận được Hành vi không xác địnhdefault sẽ vô nghĩa. Không có gì mà bạn có thể làm để làm điều này tốt hơn.

Hãy để tôi rõ ràng hơn. Ngay khi ai đó chuyển một hàm chưa được khởi tạo intvào chức năng của bạn, đó là Hành vi không xác định. Chức năng của bạn có thể giải quyết vấn đề Dừng và nó không thành vấn đề. Đó là UB. Không có gì bạn có thể làm một khi UB đã được gọi.


3
Làm thế nào về việc đăng nhập nó hoặc ném một ngoại lệ thay vì nếu âm thầm bỏ qua nó?
jgauffin

3
Thật vậy, có rất nhiều việc có thể được thực hiện, chẳng hạn như ... thêm một câu lệnh mặc định vào công tắc và xử lý lỗi một cách duyên dáng. Điều này được gọi là lập trình phòng thủ . Nó sẽ không chỉ bảo vệ chống lại lập trình viên câm khi xử lý chức năng đầu vào không chính xác mà còn chống lại các lỗi khác nhau do tràn bộ đệm, tràn ngăn xếp, mã chạy, v.v.

1
Không, nó sẽ không xử lý bất cứ điều gì. Đó là UB. Chương trình có UB thời điểm chức năng được nhập và cơ thể chức năng không thể làm gì về nó.
DeadMG

2
@DeadMG Một enum có thể có bất kỳ giá trị nào mà kiểu số nguyên tương ứng của nó trong quá trình thực hiện có thể có. Không có gì không xác định về nó. Truy cập nội dung của biến tự động chưa được khởi tạo thực sự là UB, nhưng "hack ác" (rất có thể là một loại lỗi) cũng có thể ném một giá trị được xác định rõ vào hàm, mặc dù đó không phải là một trong các giá trị được liệt kê trong tờ khai enum.

2

Tuyên bố mặc định sẽ không nhất thiết phải giúp đỡ. Nếu công tắc vượt quá enum, bất kỳ giá trị nào không được xác định trong enum sẽ kết thúc thực hiện hành vi không xác định.

Đối với tất cả những gì bạn biết, trình biên dịch có thể biên dịch công tắc đó (với mặc định) là:

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

Một khi bạn kích hoạt hành vi không xác định, sẽ không quay trở lại.


2
Thứ nhất, đó là một giá trị không xác định , không phải là hành vi không xác định. Điều này có nghĩa là trình biên dịch có thể giả sử một số giá trị khác thuận tiện hơn, nhưng nó có thể không biến mặt trăng thành phô mai xanh, v.v. Thứ hai, theo như tôi có thể nói, điều đó chỉ áp dụng cho C ++. Trong C, phạm vi giá trị của một enumloại giống như phạm vi giá trị của loại số nguyên cơ bản của nó (được xác định theo triển khai, do đó phải tự đồng nhất).
Jens Ayton

1
Tuy nhiên, một thể hiện của biến enum và hằng số liệt kê không nhất thiết phải có cùng độ rộng và độ ký, cả hai đều được xác định. Nhưng tôi khá chắc chắn rằng tất cả các enum trong C được đánh giá chính xác như kiểu số nguyên tương ứng của chúng, do đó không có hành vi không xác định hoặc thậm chí không xác định ở đó.

2

Tôi cũng thích có một default:trong tất cả các trường hợp. Tôi đến bữa tiệc muộn như thường lệ, nhưng ... một số suy nghĩ khác mà tôi không thấy ở trên:

  • Cảnh báo cụ thể (hoặc lỗi nếu ném cũng -Werror) đến từ -Wcovered-switch-default(từ -Weverythingnhưng không -Wall). Nếu sự linh hoạt về đạo đức của bạn cho phép bạn tắt một số cảnh báo nhất định (ví dụ: loại bỏ một số thứ từ -Wallhoặc -Weverything), hãy xem xét việc ném-Wno-covered-switch-default (hoặc -Wno-error=covered-switch-defaultkhi sử dụng -Werror) và nói chung -Wno-...cho các cảnh báo khác mà bạn thấy không đồng ý.
  • Đối với gcc(và hành vi chung chung hơn trong clang), tham khảo ý kiến gccmanpage cho -Wswitch, -Wswitch-enum, -Wswitch-defaultcho hành vi (khác nhau) trong những tình huống tương tự các loại được liệt kê trong báo cáo chuyển đổi.
  • Tôi cũng không thích cảnh báo này trong khái niệm và tôi không thích từ ngữ của nó; với tôi, các từ trong cảnh báo ("nhãn mặc định ... bao gồm tất cả ... giá trị") gợi ý rằngdefault: trường hợp này sẽ luôn được thực thi, chẳng hạn như

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }

    Mở bài đọc thứ nhất, đây là những gì tôi nghĩ bạn đang chạy vào - mà bạn default:trường hợp sẽ luôn luôn được thực hiện vì không có break;hoặc return;hoặc tương đương. Khái niệm này tương tự (với tai tôi) với các bình luận kiểu bảo mẫu khác (mặc dù đôi khi hữu ích) xuất hiện từ đó clang. Nếufoo == 1 , cả hai chức năng sẽ được thực thi; mã của bạn ở trên có hành vi này. Tức là, không thoát ra được nếu bạn muốn tiếp tục thực thi mã từ các trường hợp tiếp theo! Điều này xuất hiện, tuy nhiên, không phải là vấn đề của bạn.

Có nguy cơ bị phạm tội, một số suy nghĩ khác cho sự hoàn chỉnh:

  • Tuy nhiên, tôi nghĩ rằng hành vi này phù hợp với việc kiểm tra kiểu tích cực trong các ngôn ngữ hoặc trình biên dịch khác. Nếu, như bạn đưa ra giả thuyết, một số reprobate không cố gắng chuyển một inthoặc một cái gì đó cho chức năng này, rõ ràng là có ý định sử dụng loại cụ thể của riêng bạn, trình biên dịch của bạn sẽ bảo vệ bạn tốt như vậy trong tình huống đó với một cảnh báo hoặc lỗi nghiêm trọng. NHƯNG không! (Đó là, có vẻ như ít nhất gccclangkhông thực hiện enumkiểm tra loại, nhưng tôi nghe thấy điều iccđó ). Vì bạn không nhận được loại an toàn, bạn có thể nhận được giá trị - an toàn như đã thảo luận ở trên. Mặt khác, như được đề xuất trong TFA, hãy xem xét mộtstruct hoặc thứ gì đó có thể cung cấp loại an toàn.
  • Một cách giải quyết khác có thể là tạo ra một "giá trị" mới enumnhư của bạn MaskValueIllegal, và không hỗ trợ điều đó casetrong của bạn switch. Điều đó sẽ được ăn bởi default:(ngoài bất kỳ giá trị lập dị nào khác)

Mã hóa phòng thủ sống lâu!


1

Đây là một gợi ý thay thế:
OP đang cố gắng bảo vệ chống lại trường hợp ai đó đi qua intnơi có enum được mong đợi. Hoặc, nhiều khả năng, nơi ai đó đã liên kết một thư viện cũ với một chương trình mới hơn bằng cách sử dụng một tiêu đề mới hơn với nhiều trường hợp hơn.

Tại sao không thay đổi công tắc để xử lý inttrường hợp? Thêm một diễn viên trước giá trị trong công tắc sẽ loại bỏ cảnh báo và thậm chí cung cấp một mức độ gợi ý về lý do tại sao mặc định tồn tại.

void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}

Tôi tìm thấy điều này nhiều ít bị phản đối hơn so với assert()thử nghiệm mỗi giá trị có thể hoặc thậm chí làm cho giả định rằng phạm vi của enum giá trị nổi ra lệnh để cho một công trình thử nghiệm đơn giản hơn. Đây chỉ là một cách xấu xí để làm những gì mặc định làm chính xác và đẹp mắt.


1
bài này khá khó đọc (tường văn bản). Bạn có phiền chỉnh sửa ing nó thành một hình dạng tốt hơn?
gnat
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.