Cú pháp hợp lệ, nhưng vô giá trị trong trường hợp chuyển đổi?


207

Thông qua một lỗi đánh máy, tôi vô tình tìm thấy cấu trúc này:

int main(void) {
    char foo = 'c';

    switch(foo)
    {
        printf("Cant Touch This\n");   // This line is Unreachable

        case 'a': printf("A\n"); break;
        case 'b': printf("B\n"); break;
        case 'c': printf("C\n"); break;
        case 'd': printf("D\n"); break;
    }

    return 0;
}

Có vẻ như printfở đầu switchtuyên bố là hợp lệ, nhưng cũng hoàn toàn không thể truy cập được.

Tôi đã có một biên dịch sạch, thậm chí không có cảnh báo về mã không thể truy cập, nhưng điều này dường như vô nghĩa.

Một trình biên dịch có thể đánh dấu mã này là mã không thể truy cập?
Điều này có phục vụ mục đích nào không?


51
GCC có một lá cờ đặc biệt cho việc này. Đó là-Wswitch-unreachable
Eli Sadoff

57
"Điều này có phục vụ mục đích nào không?" Chà, bạn có thể gotovào và ra khỏi phần không thể truy cập khác, có thể hữu ích cho các bản hack khác nhau.
HolyBlackCat

12
@HolyBlackCat Sẽ không như vậy đối với tất cả các mã không thể truy cập?
Eli Sadoff

28
@EliSadoff Thật vậy. Tôi đoán nó không phục vụ bất kỳ mục đích đặc biệt nào . Tôi cá là nó được cho phép chỉ vì không có lý do gì để cấm cả. Rốt cuộc, switchchỉ là một điều kiện gotovới nhiều nhãn. Có ít nhiều hạn chế giống nhau trên cơ thể của nó như bạn có trên một khối mã thông thường chứa đầy nhãn goto.
HolyBlackCat

16
Worth chỉ ra rằng ví dụ của @MooingDuck là một biến thể trên thiết bị của Duff ( en.wikipedia.org/wiki/Duff's_device )
Michael Anderson

Câu trả lời:


226

Có lẽ không phải là hữu ích nhất, nhưng không hoàn toàn vô giá trị. Bạn có thể sử dụng nó để khai báo một biến cục bộ có sẵn trong switchphạm vi.

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

Tiêu chuẩn ( N1579 6.8.4.2/7) có mẫu sau:

VÍ DỤ Trong đoạn chương trình nhân tạo

switch (expr)
{
    int i = 4;
    f(i);
case 0:
    i = 17;
    /* falls through into default code */
default:
    printf("%d\n", i);
}

đối tượng có mã định danh itồn tại với thời lượng lưu trữ tự động (trong khối) nhưng không bao giờ được khởi tạo và do đó, nếu biểu thức điều khiển có giá trị khác 0, lệnh gọi printfhàm sẽ truy cập giá trị không xác định. Tương tự, cuộc gọi đến chức năng fkhông thể đạt được.

PS BTW, mẫu không phải là mã C ++ hợp lệ. Trong trường hợp đó ( N4140 6.7/3, nhấn mạnh của tôi):

Một chương trình nhảy 90 từ một điểm trong đó một biến có thời gian lưu trữ tự động không nằm trong phạm vi đến một điểm có phạm vi không được định dạng trừ khi biến đó có kiểu vô hướng , loại lớp với hàm tạo mặc định tầm thường và hàm hủy tầm thường, một phiên bản đủ điều kiện cv của một trong những loại này hoặc một mảng của một trong các loại trước đó và được khai báo mà không có bộ khởi tạo (8.5).


90) Việc chuyển từ điều kiện của một switchtuyên bố sang nhãn trường hợp được coi là một bước nhảy trong khía cạnh này.

Vì vậy, thay thế int i = 4;bằng int i;làm cho nó một C ++ hợp lệ.


3
"... nhưng không bao giờ được khởi tạo ..." Có vẻ như iđược khởi tạo thành 4, tôi còn thiếu gì?
yano

7
Lưu ý rằng nếu biến là static, nó sẽ được khởi tạo về 0, vì vậy cũng có cách sử dụng an toàn cho việc này.
Leushenko

23
@yano Chúng tôi luôn nhảy qua i = 4;khởi tạo, vì vậy nó không bao giờ diễn ra.
AlexD

12
Tất nhiên rồi! ... toàn bộ quan điểm của câu hỏi ... trời ạ. Mong muốn mạnh mẽ là xóa đi sự ngu ngốc này
yano

1
Đẹp! Đôi khi tôi cần một biến tạm thời bên trong a casevà luôn phải sử dụng các tên khác nhau trong mỗi casehoặc xác định nó bên ngoài công tắc.
SJuan76

98

Điều này có phục vụ mục đích nào không?

Đúng. Nếu thay vì một tuyên bố, bạn đặt một tuyên bố trước nhãn đầu tiên, điều này có thể có ý nghĩa hoàn hảo:

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

Các quy tắc khai báo và câu lệnh được chia sẻ cho các khối nói chung, do đó, đó là quy tắc tương tự cho phép điều đó cũng cho phép các câu lệnh ở đó.


Cũng đáng nói là nếu câu lệnh đầu tiên là cấu trúc vòng lặp, nhãn trường hợp có thể xuất hiện trong thân vòng lặp:

switch (i) {
  for (;;) {
    f();
  case 1:
    g();
  case 2:
    if (h()) break;
  }
}

Vui lòng không viết mã như thế này nếu có cách viết dễ đọc hơn, nhưng nó hoàn toàn hợp lệ và f()cuộc gọi có thể truy cập được.


Thiết bị của @MatthieuM Duff có nhãn trường hợp bên trong một vòng lặp, nhưng bắt đầu bằng nhãn trường hợp trước vòng lặp.

2
Tôi không chắc là tôi nên upvote cho ví dụ thú vị hay downvote cho sự điên rồ hoàn toàn khi viết điều này trong một chương trình thực sự :). Chúc mừng bạn đã lặn xuống vực thẳm và trở về trong một mảnh.
Liviu T.

@ChemicalEngineer: Nếu mã là một phần của vòng lặp, vì nó nằm trong Thiết bị của Duff, { /*code*/ switch(x) { } }có thể trông sạch hơn nhưng nó cũng sai .
Ben Voigt

39

Có một cách sử dụng nổi tiếng này gọi là Thiết bị của Duff .

int n = (count+3)/4;
switch (count % 4) {
  do {
    case 0: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
  } while (--n > 0);
}

Ở đây chúng tôi sao chép một bộ đệm được trỏ đến bởi frommột bộ đệm được chỉ bởi to. Chúng tôi sao chép các counttrường hợp dữ liệu.

Câu do{}while()lệnh bắt đầu trước casenhãn đầu tiên và các casenhãn được nhúng trong do{}while().

Điều này làm giảm số lượng các nhánh có điều kiện ở cuối do{}while()vòng lặp gặp phải bởi khoảng 4 nhân tố (trong ví dụ này; hằng số có thể được điều chỉnh theo bất kỳ giá trị nào bạn muốn).

Bây giờ, trình tối ưu hóa đôi khi có thể làm điều này cho bạn (đặc biệt nếu chúng đang tối ưu hóa các hướng dẫn truyền phát / véc tơ), nhưng không có tối ưu hóa hướng dẫn hồ sơ, chúng không thể biết liệu bạn có mong đợi vòng lặp lớn hay không.

Nói chung, khai báo biến có thể xảy ra ở đó và được sử dụng trong mọi trường hợp, nhưng nằm ngoài phạm vi sau khi kết thúc chuyển đổi. (lưu ý mọi khởi tạo sẽ bị bỏ qua)

Ngoài ra, luồng điều khiển không dành riêng cho chuyển đổi có thể đưa bạn vào phần đó của khối chuyển đổi, như được minh họa ở trên hoặc với a goto.


2
Tất nhiên, điều này vẫn có thể xảy ra nếu không cho phép các tuyên bố trên trường hợp đầu tiên, vì thứ tự do {case 0:không quan trọng, cả hai đều phục vụ để đặt mục tiêu nhảy lên đầu tiên *to = *from++;.
Ben Voigt

1
@BenVoigt Tôi cho rằng việc đặt do {nó dễ đọc hơn. Phải, tranh luận về khả năng đọc cho Thiết bị của Duff là ngu ngốc và vô nghĩa và có thể là một cách đơn giản để phát điên.
Vụ kiện của Quỹ Monica

1
@QPaysTaxes Bạn nên kiểm tra Simon Tatham của coroutines trong C . Hoặc có thể không.
Jonas Schäfer

@ JonasSchäfer Thật thú vị, đó là những gì mà C ++ 20 coroutines sẽ làm cho bạn.
Yakk - Adam Nevraumont

15

Giả sử bạn đang sử dụng gcc trên Linux, nó sẽ đưa ra cảnh báo nếu bạn đang sử dụng phiên bản 4.4 trở về trước.

Tùy chọn -Wunreachable-code đã bị xóa trong gcc 4.4 trở đi.


Có kinh nghiệm vấn đề đầu tiên luôn luôn giúp!
16 giờ

@JonathanLeffler: Vấn đề chung về cảnh báo gcc dễ bị ảnh hưởng bởi các bộ tối ưu hóa cụ thể được chọn vẫn là sự thật, thật không may, và làm cho trải nghiệm người dùng kém. Thật khó chịu khi có một bản dựng Debug sạch sẽ, sau đó là một bản phát hành thất bại: /
Matthieu M.

@MatthieuM.: Có vẻ như một cảnh báo như vậy sẽ cực kỳ dễ dàng phát hiện trong các trường hợp liên quan đến ngữ pháp thay vì phân tích ngữ nghĩa [ví dụ mã theo sau "nếu" trả về cả hai nhánh] và giúp dễ dàng ngăn chặn "sự khó chịu" cảnh báo. Mặt khác, có một số trường hợp rất hữu ích khi có lỗi hoặc cảnh báo trong các bản dựng phát hành không có trong các bản dựng gỡ lỗi (nếu không có gì khác, ở những nơi mà một bản hack được đưa vào để gỡ lỗi được cho là sẽ được dọn sạch giải phóng).
supercat

1
@MatthieuM.: Nếu phân tích ngữ nghĩa quan trọng sẽ được yêu cầu để phát hiện ra rằng một biến nào đó sẽ luôn sai ở một vị trí nào đó trong mã, mã có điều kiện dựa trên biến đó là đúng sẽ chỉ có thể truy cập được nếu phân tích đó được thực hiện. Mặt khác, tôi sẽ xem xét một thông báo rằng mã đó không thể truy cập được khác với cảnh báo về mã không thể truy cập theo cú pháp, vì nó hoàn toàn bình thường khi có một số điều kiện có thể xảy ra với một số cấu hình dự án chứ không phải các cấu hình dự án khác. Đôi khi nó có thể hữu ích cho các lập trình viên ...
supercat

1
... để biết lý do tại sao một số cấu hình tạo mã lớn hơn các cấu hình khác [ví dụ: vì trình biên dịch có thể coi một số điều kiện là không thể trong một cấu hình nhưng không phải là khác] nhưng điều đó không có nghĩa là có bất cứ điều gì "sai" với mã có thể được tối ưu hóa trong thời trang đó với một số cấu hình.
supercat

11

Không chỉ cho khai báo biến mà nhảy cao. Bạn có thể sử dụng nó tốt nếu và chỉ khi bạn không thiên về mã spaghetti.

int main()
{
    int i = 1;
    switch(i)
    {
        nocase:
        printf("no case\n");

        case 0: printf("0\n"); break;
        case 1: printf("1\n"); goto nocase;
    }
    return 0;
}

Bản in

1
no case
0 /* Notice how "0" prints even though i = 1 */

Cần lưu ý rằng trường hợp chuyển đổi là một trong những mệnh đề dòng điều khiển nhanh nhất. Vì vậy, nó phải rất linh hoạt cho các lập trình viên, đôi khi liên quan đến các trường hợp như thế này.


Và sự khác biệt giữa nocase:và là default:gì?
i486

@ i486 Khi i=4nó không kích hoạt nocase.
Sanchke Dellowar

@SanchkeDellowar đó là những gì tôi muốn nói.
njzk2

Tại sao người ta sẽ làm điều đó thay vì chỉ đơn giản là đặt trường hợp 1 trước trường hợp 0 ​​và sử dụng dự phòng?
Jonas Schäfer

@JonasWielicki Trong mục tiêu này, bạn có thể làm điều đó. Nhưng mã này chỉ là một trường hợp hiển thị của những gì có thể được thực hiện.
Sanchke Dellowar

11

Cần lưu ý rằng hầu như không có hạn chế về cấu trúc đối với mã trong switchcâu lệnh hoặc nơi case *:nhãn được đặt trong mã này *. Điều này làm cho các thủ thuật lập trình như thiết bị của duff trở nên khả thi, một cách thực hiện có thể giống như thế này:

int n = ...;
int iterations = n/8;
switch(n%8) {
    while(iterations--) {
        sum += *ptr++;
        case 7: sum += *ptr++;
        case 6: sum += *ptr++;
        case 5: sum += *ptr++;
        case 4: sum += *ptr++;
        case 3: sum += *ptr++;
        case 2: sum += *ptr++;
        case 1: sum += *ptr++;
        case 0: ;
    }
}

Bạn thấy đấy, mã giữa switch(n%8) {case 7:nhãn chắc chắn có thể truy cập ...


* Như supercat đã chỉ ra một cách may mắn trong một nhận xét : Kể từ C99, không phải gotonhãn hay nhãn (có thể là case *:nhãn hay không) có thể xuất hiện trong phạm vi của một tuyên bố có chứa khai báo VLA. Vì vậy, không đúng khi nói rằng không có hạn chế về cấu trúc đối với vị trí của case *:nhãn. Tuy nhiên, thiết bị của duff có trước tiêu chuẩn C99 và dù sao nó cũng không phụ thuộc vào VLA. Tuy nhiên, tôi cảm thấy buộc phải chèn "hầu như" vào câu đầu tiên của mình do điều này.


Việc bổ sung các mảng có độ dài thay đổi dẫn đến việc áp đặt các hạn chế về cấu trúc liên quan đến chúng.
supercat

@supercat Những loại hạn chế?
cmaster - phục hồi monica

1
Không một nhãn gotocũng không switch/case/defaultthể xuất hiện trong phạm vi của bất kỳ đối tượng hoặc loại được khai báo thay đổi. Điều này có nghĩa là nếu một khối chứa bất kỳ khai báo nào về các đối tượng hoặc kiểu mảng có độ dài thay đổi, thì bất kỳ nhãn nào cũng phải đi trước các khai báo đó. Có một chút khó hiểu trong Tiêu chuẩn, điều này cho thấy rằng trong một số trường hợp, phạm vi của một tuyên bố VLA mở rộng đến toàn bộ câu lệnh chuyển đổi; xem stackoverflow.com/questions/41752072/ cho câu hỏi của tôi về điều đó.
supercat

@supercat: Bạn chỉ hiểu nhầm rằng verbiage (mà tôi đoán là lý do tại sao bạn xóa câu hỏi của bạn). Nó áp đặt một yêu cầu về phạm vi mà VLA có thể được xác định. Nó không mở rộng phạm vi đó, nó chỉ làm cho các định nghĩa VLA nhất định không hợp lệ.
Keith Thompson

@KeithThndry: Vâng, tôi đã hiểu nhầm nó. Việc sử dụng kỳ lạ của thì hiện tại trong phần chú thích khiến mọi thứ trở nên khó hiểu và tôi nghĩ rằng khái niệm này có thể được thể hiện tốt hơn như là một lệnh cấm: phạm vi của tuyên bố VLA đó ".
supercat

10

Bạn đã có câu trả lời của mình liên quan đến tùy chọn bắt buộcgcc -Wswitch-unreachable để tạo cảnh báo, câu trả lời này là để giải thích về phần khả dụng / tính xứng đáng .

Trích dẫn thẳng ra C11, chương §6.8.4.2, ( nhấn mạnh của tôi )

switch (expr)
{
int i = 4;
f(i);
case 0:
i = 17;
/* falls through into default code */
default:
printf("%d\n", i);
}

đối tượng có mã định danh itồn tại với thời lượng lưu trữ tự động (trong khối) nhưng không bao giờ được khởi tạo và do đó, nếu biểu thức điều khiển có giá trị khác 0, lệnh gọi printf hàm sẽ truy cập giá trị không xác định. Tương tự, cuộc gọi đến chức năng fkhông thể đạt được.

Đó là rất tự giải thích. Bạn có thể sử dụng điều này để xác định một biến phạm vi cục bộ chỉ có sẵn trong switchphạm vi câu lệnh.


9

Có thể thực hiện "vòng lặp rưỡi" với nó, mặc dù nó có thể không phải là cách tốt nhất để làm điều đó:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));

@RichardII Có phải là chơi chữ hay không? Vui lòng giải thích.
Dancia

1
@Dancia Anh ấy nói rằng điều này khá rõ ràng không phải là cách tốt nhất để làm điều gì đó như thế này, và "có thể không" là một điều gì đó không đúng.
Vụ kiện của Quỹ Monica
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.