Tại sao phiên bản này của AND logic trong C không hiển thị hành vi ngắn mạch?


80

Vâng, đây là một câu hỏi bài tập về nhà, nhưng tôi đã thực hiện nghiên cứu của mình và suy nghĩ rất nhiều về chủ đề này và không thể tìm ra điều này. Câu hỏi nói rằng đoạn mã này KHÔNG thể hiện hành vi ngắn mạch và hỏi tại sao. Nhưng với tôi thì có vẻ như nó biểu hiện hành vi đoản mạch, vậy ai đó có thể giải thích tại sao nó không?

Trong C:

int sc_and(int a, int b) {
    return a ? b : 0;
}

Tôi thấy rằng trong trường hợp đó alà sai, chương trình sẽ không cố gắng đánh giá bgì cả, nhưng tôi phải sai. Tại sao chương trình thậm chí cảm ứng btrong trường hợp này, trong khi nó không phải như vậy?


11
Như là điển hình của hầu hết các câu hỏi bài tập về nhà có nội dung, bạn sẽ không bao giờ thấy loại mã này trong hệ thống sản xuất, trừ khi lập trình viên cố tình cố tỏ ra thông minh.
Robert Harvey

49
@RobertHarvey Bạn sẽ luôn thấy mã giống như thế này trong các hệ thống sản xuất! Nó không có khả năng là một hàm được đặt tên AND(), nhưng các hàm nhận các đối số theo giá trị và sau đó đánh giá chúng (hoặc không) tùy thuộc vào logic của hàm ở khắp mọi nơi. Mặc dù là một "câu hỏi mẹo", đó là một hành vi quan trọng của C để hiểu. Trong một ngôn ngữ có cách gọi tên, câu hỏi này sẽ có một câu trả lời rất khác.
Ben Jackson

7
@BenJackson: Tôi đã nhận xét về mã, không phải hành vi. Có, bạn cần phải hiểu hành vi. Không, bạn không cần phải viết mã như thế này.
Robert Harvey

3
Điều này thực sự khá phù hợp nếu bạn phải viết mã trong VB và gặp phải IIf. Bởi vì nó là một chức năng hơn là một nhà điều hành, đánh giá không bị đoản mạch. Điều này có thể tạo ra vấn đề cho các nhà phát triển đã quen với toán tử đoản mạch, những người sau đó viết những thứ như thế IIf(x Is Nothing, Default, x.SomeValue).
Dan Bryant

3
@alk bởi vì nó là một bản cáo trạng của hệ thống giáo dục.
Shivan Dragon

Câu trả lời:


118

Đây là một câu hỏi lừa đảo. blà một đối số đầu vào cho sc_andphương thức và vì vậy sẽ luôn được đánh giá. Nói cách khác sc_and(a(), b())sẽ gọi a()và gọi b()(thứ tự không được đảm bảo), sau đó gọi sc_andvới kết quả a(), b()được chuyển đến a?b:0. Nó không liên quan gì đến chính nhà điều hành bậc ba, điều này sẽ hoàn toàn gây ngắn mạch.

CẬP NHẬT

Về lý do tại sao tôi gọi đây là một 'câu hỏi mẹo': Đó là vì thiếu bối cảnh được xác định rõ ràng để xem xét 'đoản mạch' ở đâu (ít nhất là do OP tái tạo). Nhiều người, khi đưa ra chỉ là một định nghĩa hàm, cho rằng bối cảnh của câu hỏi được hỏi về cơ thể của hàm ; họ thường không coi hàm như một biểu thức trong và của chính nó. Đây là 'mẹo' của câu hỏi; Nhắc nhở bạn rằng trong lập trình nói chung, nhưng đặc biệt là trong các ngôn ngữ như C-like thường có nhiều ngoại lệ cho các quy tắc, bạn không thể làm điều đó. Ví dụ, nếu câu hỏi được hỏi như vậy:

Hãy xem xét đoạn mã sau. Sc_and sẽ loại bỏ hành vi ngắn mạch khi được gọi từ main :

int sc_and(int a, int b){
    return a?b:0;
}

int a(){
    cout<<"called a!"<<endl;
    return 0;
}

int b(){
    cout<<"called b!"<<endl;
    return 1;
}

int main(char* argc, char** argv){
    int x = sc_and(a(), b());
    return 0;
}

Rõ ràng ngay lập tức rằng bạn phải nghĩ đến sc_andnhư một nhà điều hành trong và của chính nó trong ngôn ngữ miền cụ thể của riêng bạn và đánh giá xem cuộc gọi có sc_andbiểu hiện hành vi ngắn mạch như thông thường hay &&không . Tôi sẽ không coi đó là một câu hỏi đánh lừa chút nào, bởi vì rõ ràng là bạn không nên tập trung vào toán tử bậc ba, và thay vào đó phải tập trung vào cơ chế gọi hàm của C / C ++ (và, tôi đoán, dẫn độc đáo thành một câu hỏi tiếp theo để viết một câu hỏi sc_andngắn mạch, liên quan đến việc sử dụng một #definechứ không phải một hàm).

Việc bạn có gọi chính toán tử bậc ba gây ra hiện tượng đoản mạch hay không (hoặc một cái gì đó khác, như 'đánh giá có điều kiện') phụ thuộc vào định nghĩa của bạn về đoản mạch và bạn có thể đọc các bình luận khác nhau để suy nghĩ về điều đó. Theo tôi thì nó đúng, nhưng nó không liên quan nhiều đến câu hỏi thực tế hoặc tại sao tôi gọi nó là 'mánh khóe'.


13
Nhà điều hành bậc ba làm ngắn mạch? Không. Nó đánh giá một trong các biểu thức sau ?, giống như trong if (condition) {when true} else {when-false}. Đó không được gọi là đoản mạch.
Jens


17
@Jens: Tôi sẽ gọi bất kỳ trường hợp nào mà nhà điều hành bỏ qua đánh giá một hoặc nhiều toán hạng của nó là một dạng "chập mạch", nhưng đây thực sự chỉ là vấn đề bạn chọn định nghĩa thuật ngữ như thế nào.
R .. GitHub DỪNG TRỢ GIÚP ICE

13
@Jens Đó là đường cú pháp cho an if else, không phải an if. Và thậm chí sau đó, nó không thực sự; các ?:nhà điều hành là một biểu hiện , if-elselà một tuyên bố . Đó là những thứ rất khác nhau về mặt ngữ nghĩa, ngay cả khi bạn có thể xây dựng một tập hợp các câu lệnh tương đương tạo ra hiệu ứng giống như biểu thức bậc ba . Đây là lý do tại sao nó được coi là ngắn mạch; hầu hết các biểu thức khác luôn đánh giá tất cả các toán hạng của chúng ( +,-,*,/v.v.). Khi chúng không, đó là ngắn mạch ( &&, ||).
aruisdante

9
Vâng, với tôi, sự khác biệt quan trọng là chúng ta đang nói về các toán tử. Tất nhiên các câu lệnh điều khiển luồng kiểm soát đường dẫn mã nào được thực thi. Đối với các toán tử, điều không rõ ràng là đối với một người không quen thuộc với ngôn ngữ C và C dẫn xuất rằng một toán tử có thể không đánh giá tất cả các toán hạng của nó, vì vậy điều quan trọng là phải có một thuật ngữ để nói về thuộc tính này, và vì vậy, tôi sử dụng "short khoanh tròn ”.
R .. GitHub NGỪNG TRỢ GIÚP ICE

43

Khi tuyên bố

bool x = a && b++;  // a and b are of int type

thực thi, b++sẽ không được đánh giá nếu toán hạng được ađánh giá là false(hành vi ngắn mạch). Điều này có nghĩa là tác dụng phụ trên bsẽ không xảy ra.

Bây giờ, hãy nhìn vào hàm:

bool and_fun(int a, int b)
{
     return a && b; 
}

và gọi cái này

bool x = and_fun(a, b++);

Trong trường hợp này, dù atruehay false, b++sẽ luôn được đánh giá 1 trong khi gọi hàm và tác dụng phụ trên bsẽ luôn xảy ra.

Điều này cũng đúng với

int x = a ? b : 0; // Short circuit behavior 

int sc_and (int a, int b) // No short circuit behavior.
{
   return a ? b : 0;
} 

1 Thứ tự đánh giá các đối số hàm là không xác định.


1
OK, rất xác đáng với câu trả lời của Stephen Quan: có thể (hợp pháp) một hàm tương tự có thể nội dòng hàm "and_fun", chẳng hạn như khi bạn gọi "bool x = and_fun (a, b ++);" b ++ sẽ không được tăng lên nếu a đúng?
Shivan Dragon

4
@ShivanDragon Điều đó có vẻ như thay đổi hành vi có thể quan sát được đối với tôi.
sapi

3
@ShivanDragon: Khi nội tuyến trình biên dịch sẽ không thay đổi hành vi. Nó không phải là đơn giản như thay thế các cơ quan chức năng ở.
Jack Aidley

Để rõ ràng hơn, bạn có thể thêm int x = a ? b++ : 0, như là hiện tượng đoản mạch có thể quan sát được.
Paul Draper

@PaulDraper; Tôi đã giữ nguyên đoạn văn bản gốc để người đọc không nhầm lẫn.
haccks

19

Như đã được chỉ ra bởi những người khác, bất kể điều gì được truyền vào hàm dưới dạng hai đối số, nó sẽ được đánh giá khi nó được chuyển vào. Đó là cách trước khi thực hiện phép toán một lần.

Mặt khác, điều này

#define sc_and(a, b) \
  ((a) ?(b) :0)

sẽ là "ngắn mạch", vì macro này không ngụ ý một lệnh gọi hàm và với điều này, không có đánh giá nào về (các) đối số của hàm được thực hiện.


có thể giải thích tại sao? Đối với tôi, nó giống hệt như đoạn trích của op. Ngoại trừ nội tuyến của nó không phải là một Phạm vi khác.
dhein

5

Đã chỉnh sửa để sửa các lỗi được lưu ý trong bình luận của @cmasters.


Trong

int sc_and(int a, int b) {
    return a ? b : 0;
}

... returnbiểu thức ed biểu hiện đánh giá ngắn mạch, nhưng lệnh gọi hàm thì không.

Thử gọi điện

sc_and (0, 1 / 0);

Lời gọi hàm đánh giá 1 / 0, mặc dù nó không bao giờ được sử dụng, do đó gây ra - có thể - lỗi chia cho 0.

Các đoạn trích liên quan từ Tiêu chuẩn ANSI C (dự thảo) là:

2.1.2.3 Thực hiện chương trình

...

Trong máy trừu tượng, tất cả các biểu thức được đánh giá như được chỉ định bởi ngữ nghĩa. Việc triển khai thực tế không cần đánh giá một phần của biểu thức nếu nó có thể suy ra rằng giá trị của nó không được sử dụng và không có tác dụng phụ cần thiết nào được tạo ra (bao gồm bất kỳ tác dụng phụ nào gây ra bởi việc gọi hàm hoặc truy cập một đối tượng dễ bay hơi).

3.3.2.2 Các lệnh gọi hàm

....

Ngữ nghĩa

...

Khi chuẩn bị cho lệnh gọi một hàm, các đối số được đánh giá và mỗi tham số được gán giá trị của đối số tương ứng.

Tôi đoán rằng mỗi đối số được đánh giá là một biểu thức, nhưng danh sách đối số nói chung không phải là một biểu thức, do đó hành vi không phải SCE là bắt buộc.

Là một người vẽ trên bề mặt của vùng nước sâu của tiêu chuẩn C, tôi đánh giá cao một cái nhìn đầy đủ thông tin về hai khía cạnh:

  • Đánh giá có 1 / 0tạo ra hành vi không xác định không?
  • Danh sách đối số có phải là một biểu thức không? (Tôi nghĩ là không)

PS

Ngay cả khi bạn chuyển sang C ++ và định nghĩa sc_andnhư một inlinehàm, bạn sẽ không nhận được SCE. Nếu bạn định nghĩa nó là một macro C, như @alk , bạn chắc chắn sẽ làm như vậy.


1
Không, một inlinehàm không thay đổi ngữ nghĩa của một lời gọi hàm. Và những ngữ nghĩa đó chỉ định rằng tất cả các đối số được đánh giá, như bạn đã trích dẫn chính xác. Ngay cả khi trình biên dịch có thể tối ưu hóa cuộc gọi, hành vi hiển thị không được thay đổi, tức là sc_and(f(), g())phải hoạt động như thể cả hai f()g()luôn được gọi. sc_and(0, 1/0)là một ví dụ xấu bởi vì nó hành vi không xác định, và trình biên dịch không cần phải gọi thậm chí sc_and()...
cmaster - Khôi phục monica

@cmaster Cảm ơn bạn. Tôi chỉ đơn giản là không biết rằng inlinengữ nghĩa cuộc gọi được bảo tồn C ++ . Tôi đã mắc kẹt với số chia không, vì nó phù hợp với ví dụ và SCE tôi nghĩ thường được khai thác để tránh hành vi không xác định.
Hình thu nhỏ vào

4

Để thấy rõ dấu chấm tròn bậc ba, hãy thử thay đổi mã một chút để sử dụng con trỏ hàm thay vì số nguyên:

int a() {
    printf("I'm a() returning 0\n");
    return 0;
}

int b() {
    printf("And I'm b() returning 1 (not that it matters)\n");
    return 1;
}

int sc_and(int (*a)(), int (*b)()) {
    a() ? b() : 0;
}

int main() {
    sc_and(a, b);
    return 0;
}

Và sau đó biên dịch nó (thậm chí hầu như KHÔNG có tối ưu hóa -O0:!). Bạn sẽ thấy b()nó không được thực thi nếu a()trả về false.

% gcc -O0 tershort.c            
% ./a.out 
I'm a() returning 0
% 

Ở đây, assembly được tạo ra trông giống như:

    call    *%rdx      <-- call a()
    testl   %eax, %eax <-- test result
    je      .L8        <-- skip if 0 (false)
    movq    -16(%rbp), %rdx
    movl    $0, %eax
    call    *%rdx      <- calls b() only if not skipped
.L8:

Vì vậy, như những người khác đã chỉ ra một cách chính xác thủ thuật câu hỏi là khiến bạn tập trung vào hành vi của toán tử bậc ba KHÔNG gây ngắn mạch (gọi đó là 'đánh giá có điều kiện') thay vì đánh giá tham số trên cuộc gọi (gọi theo giá trị) KHÔNG ngắn mạch.


0

Toán tử bậc ba C không bao giờ có thể ngắn mạch, bởi vì nó chỉ đánh giá một biểu thức duy nhất a (điều kiện), để xác định một giá trị được cho bởi biểu thức bc , nếu bất kỳ giá trị nào có thể được trả về.

Đoạn mã sau:

int ret = a ? b : c; // Here, b and c are expressions that return a value.

Nó gần như tương đương với mã sau:

int ret;
if(a) {ret = b} else {ret = c}

Biểu thức a có thể được tạo bởi các toán tử khác như && hoặc || điều đó có thể gây ngắn mạch vì chúng có thể đánh giá hai biểu thức trước khi trả về một giá trị, nhưng điều đó sẽ không được coi là toán tử bậc ba thực hiện ngắn mạch mà là toán tử được sử dụng trong điều kiện như trong câu lệnh if thông thường.

Cập nhật:

Có một số tranh luận về toán tử bậc ba là toán tử ngắn mạch. Lập luận cho biết bất kỳ toán tử nào không đánh giá tất cả các toán hạng của nó sẽ gây ngắn mạch theo @aruisdante trong nhận xét bên dưới. Nếu đưa ra định nghĩa này, thì toán tử bậc ba sẽ bị đoản mạch và trong trường hợp đây là định nghĩa ban đầu, tôi đồng ý. Vấn đề là thuật ngữ "ngắn mạch" ban đầu được sử dụng cho một loại toán tử cụ thể cho phép hành vi này và đó là các toán tử logic / boolean, và lý do tại sao chỉ có những điều đó là những gì tôi sẽ cố gắng giải thích.

Tiếp theo bài viết Đánh giá ngắn mạch, đánh giá ngắn mạch chỉ được đề cập đến các toán tử boolean được triển khai sang ngôn ngữ theo cách mà biết rằng toán hạng đầu tiên sẽ làm cho toán hạng thứ hai không liên quan, điều này có nghĩa là, đối với toán tử && là toán hạng đầu tiên sai , và cho || toán tử đầu tiên là true , thông số C11 cũng ghi chú nó trong 6.5.13 toán tử logic AND và 6.5.14 toán tử logic OR.

Điều này có nghĩa là để xác định hành vi ngắn mạch, bạn sẽ xác định nó trong một toán tử phải đánh giá tất cả các toán hạng giống như các toán tử boolean nếu toán hạng đầu tiên không làm cho toán hạng thứ hai không liên quan. Điều này phù hợp với những gì được viết trong một định nghĩa khác về ngắn mạch trong MathWorks trong phần "Đoản mạch logic", vì ngắn mạch xuất phát từ các toán tử logic.

Như tôi đã cố gắng giải thích toán tử bậc ba C, còn được gọi là bậc ba nếu, chỉ đánh giá hai trong số các toán hạng, nó đánh giá toán hạng đầu tiên và sau đó đánh giá toán tử thứ hai, một trong hai toán tử còn lại tùy thuộc vào giá trị của đầu tiên. Nó luôn làm điều này, nó không được cho là đánh giá cả ba trong bất kỳ tình huống nào, vì vậy không có "đoản mạch" trong bất kỳ trường hợp nào.

Như mọi khi, nếu bạn thấy điều gì đó không ổn, vui lòng viết bình luận với lập luận chống lại điều này chứ không chỉ phản đối, điều đó chỉ làm cho trải nghiệm SO tồi tệ hơn và tôi tin rằng chúng ta có thể là một cộng đồng tốt hơn nhiều mà một cộng đồng chỉ phản đối câu trả lời một người không đồng ý với.


3
Và tại sao lại ủng hộ? Tôi muốn nhận xét để tôi có thể sửa chữa bất cứ điều gì tôi đã nói sai. Hãy mang tính xây dựng.
Adrián Pérez

2
Có lẽ -1 vì nó không phải là 100% trên chủ đề. nhưng dù sao thì tôi cũng đã +1 vì câu trả lời của bạn ít nhất cũng giải thích nó rõ ràng và nó có vẻ không sai ompv.
dhein

4
@AdrianPerez xem phần nhận xét trong câu trả lời của tôi để biết lý do tại sao hầu hết mọi người 100% coi toán tử bậc ba là ngắn mạch. Đó là một biểu thức không đánh giá tất cả các toán hạng của nó dựa trên giá trị của một số toán hạng của nó. Đó là ngắn mạch.
aruisdante

3
Tôi không thể nghĩ ra bất kỳ toán tử không boolean nào không đánh giá tất cả các toán hạng của nó. Nhưng đó là bên cạnh điểm; lý do nó được gọi là đoản mạch là Preisley bởi vì nó là một toán tử (hoặc theo nghĩa chung, một biểu thức chứ không phải là một câu lệnh) không đánh giá tất cả các toán hạng của nó. Loại nhà điều hành không thực sự quan trọng. Hoàn toàn có thể viết các toán tử boolean không gây ngắn mạch và bạn cũng có thể viết các toán tử không boolean cũng như vậy (ví dụ: số nguyên muliplicaton của 0 luôn là 0, vì vậy trả về 0 ngay lập tức nếu toán hạng đầu tiên là 0).
aruisdante

2
x = a ? b : 0;là ngữ nghĩa tương tự như(a && (x = b)) || (x = 0);
jxh
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.