Tại sao chảy ra khỏi phần cuối của hàm không rỗng mà không trả về giá trị không tạo ra lỗi trình biên dịch?


158

Kể từ khi tôi nhận ra nhiều năm trước, rằng điều này không tạo ra lỗi theo mặc định (ít nhất là trong GCC), tôi luôn tự hỏi tại sao?

Tôi hiểu rằng bạn có thể đưa ra các cờ biên dịch để đưa ra cảnh báo, nhưng không phải lúc nào nó cũng là một lỗi? Tại sao nó có ý nghĩa đối với hàm không rỗng không trả về giá trị là hợp lệ?

Một ví dụ theo yêu cầu trong các ý kiến:

#include <stdio.h>
int stringSize()
{
}

int main()
{
    char cstring[5];
    printf( "the last char is: %c\n", cstring[stringSize()-1] ); 
    return 0;
}

... biên dịch.


9
Ngoài ra, tôi coi tất cả các cảnh báo tuy tầm thường như lỗi và tôi kích hoạt tất cả các cảnh báo tôi có thể (với việc hủy kích hoạt cục bộ nếu cần ... nhưng sau đó thì rõ ràng trong mã tại sao).
Matthieu M.

8
-Werror=return-typesẽ coi cảnh báo đó là một lỗi. Tôi chỉ bỏ qua cảnh báo và vài phút thất vọng theo dõi một thiscon trỏ không hợp lệ dẫn tôi đến đây và đi đến kết luận này.
jozxyqk

Điều này trở nên tồi tệ hơn bởi thực tế là việc chảy hết phần cuối của std::optionalhàm mà không trả về sẽ trả về tùy chọn "đúng"
Rufus

@Rufus Nó không phải. Đó chỉ là những gì đã xảy ra trên chu kỳ máy / trình biên dịch / HĐH / âm lịch của bạn. Bất cứ mã rác nào mà trình biên dịch tạo ra do hành vi không xác định chỉ xảy ra trông giống như một tùy chọn 'đúng', bất kể đó là gì.
gạch dưới

Câu trả lời:


146

Các tiêu chuẩn C99 và C ++ không yêu cầu các hàm trả về giá trị. Câu lệnh trả về bị thiếu trong hàm trả về giá trị sẽ chỉ được xác định (để trả về 0) trong mainhàm.

Cơ sở lý luận bao gồm việc kiểm tra xem mọi đường dẫn mã có trả về một giá trị hay không và giá trị trả về có thể được đặt bằng trình biên dịch nhúng hoặc các phương thức phức tạp khác.

Từ dự thảo C ++ 11 :

§ 6.6.3 / 2

Việc kết thúc một hàm [...] dẫn đến hành vi không xác định trong hàm trả về giá trị.

§ 3.6.1 / 5

Nếu kiểm soát đến cuối mainmà không gặp phải một return tuyên bố, thì hiệu quả là việc thực thi

return 0;

Lưu ý rằng hành vi được mô tả trong C ++ 6.6.3 / 2 không giống với C.


gcc sẽ đưa ra cảnh báo nếu bạn gọi nó với tùy chọn -Wreturn-type.

-Wreturn-type Warn bất cứ khi nào một hàm được định nghĩa với kiểu trả về mặc định là int. Đồng thời cảnh báo về bất kỳ câu lệnh trả về nào không có giá trị trả về trong hàm có kiểu trả về không có giá trị (rơi khỏi phần cuối của thân hàm được coi là trả về không có giá trị) và về câu lệnh trả về có biểu thức trong hàm có Kiểu trả về là void.

Cảnh báo này được bật bởi -Wall .


Chỉ là một sự tò mò, hãy xem những gì mã này làm:

#include <iostream>

int foo() {
   int a = 5;
   int b = a + 1;
}

int main() { std::cout << foo() << std::endl; } // may print 6

Mã này có hành vi chính thức không xác định và trong thực tế, nó gọi quy ước và phụ thuộc kiến trúc . Trên một hệ thống cụ thể, với một trình biên dịch cụ thể, giá trị trả về là kết quả của đánh giá biểu thức cuối cùng, được lưu trữ trong thanh eaxghi của bộ xử lý của hệ thống đó.


13
Tôi sẽ cảnh giác khi gọi hành vi không xác định là "được phép", mặc dù phải thừa nhận rằng tôi cũng đã sai khi gọi đó là "bị cấm". Không phải là một lỗi và không yêu cầu chẩn đoán không hoàn toàn giống như "được phép". Ít nhất, câu trả lời của bạn đọc hơi giống như bạn nói rằng nó ổn để làm, nhưng phần lớn là không.
Các cuộc đua nhẹ nhàng trong quỹ đạo

4
@Catskul, tại sao bạn mua lập luận đó? Sẽ không khả thi, nếu không dễ dàng để xác định các điểm thoát của hàm và đảm bảo tất cả chúng đều trả về một giá trị (và giá trị của kiểu trả về được khai báo)?
BlueBomber

3
@Catskul, có và không. Các ngôn ngữ được nhập và / hoặc biên dịch tĩnh thực hiện rất nhiều thứ mà bạn có thể cho là "đắt đỏ", nhưng vì chúng chỉ làm một lần, tại thời điểm biên dịch, chúng có chi phí không đáng kể. Ngay cả khi đã nói rằng, tôi không thấy lý do tại sao xác định các điểm thoát của hàm cần phải siêu tuyến tính: Bạn chỉ cần duyệt qua AST của hàm và tìm kiếm các cuộc gọi trở lại hoặc thoát. Đó là thời gian tuyến tính, được quyết định hiệu quả.
BlueBomber

3
@LightnessRacesinOrbit: Nếu một hàm có giá trị trả về đôi khi trả về ngay lập tức với một giá trị và đôi khi gọi một hàm khác luôn thoát qua throwhoặc longjmp, trình biên dịch có yêu cầu không thể truy cập returntheo lệnh gọi đến hàm không trả về không? Trường hợp không cần thiết không phổ biến và yêu cầu đưa nó vào ngay cả trong những trường hợp như vậy có lẽ sẽ không được chấp nhận, nhưng quyết định không yêu cầu là hợp lý.
supercat

1
@supercat: Một trình biên dịch siêu thông minh sẽ không cảnh báo hoặc lỗi trong trường hợp như vậy, nhưng - một lần nữa - điều này về cơ bản là không thể áp dụng cho trường hợp chung, vì vậy bạn bị mắc kẹt với một quy tắc chung. Tuy nhiên, nếu bạn biết rằng chức năng cuối của bạn sẽ không bao giờ đạt được, thì bạn đã ở rất xa ngữ nghĩa của việc xử lý chức năng truyền thống, vâng, bạn có thể tiếp tục và làm điều này và biết rằng nó an toàn. Thành thật mà nói, bạn là một lớp dưới C ++ tại thời điểm đó và, như vậy, tất cả các đảm bảo của nó là không cần thiết.
Các cuộc đua nhẹ nhàng trong quỹ đạo

42

gcc không theo mặc định kiểm tra rằng tất cả các đường dẫn mã trả về một giá trị bởi vì nói chung điều này không thể được thực hiện. Nó giả định rằng bạn biết những gì bạn đang làm. Hãy xem xét một ví dụ phổ biến bằng cách sử dụng bảng liệt kê:

Color getColor(Suit suit) {
    switch (suit) {
        case HEARTS: case DIAMONDS: return RED;
        case SPADES: case CLUBS:    return BLACK;
    }

    // Error, no return?
}

Bạn lập trình viên biết rằng, chặn một lỗi, phương thức này luôn trả về một màu. gcc tin tưởng rằng bạn biết những gì bạn đang làm vì vậy nó không buộc bạn phải trả lại ở dưới cùng của hàm.

Mặt khác, javac cố gắng xác minh rằng tất cả các đường dẫn mã trả về một giá trị và đưa ra lỗi nếu điều đó không thể chứng minh rằng tất cả chúng đều làm như vậy. Lỗi này được quy định bởi đặc tả ngôn ngữ Java. Lưu ý rằng đôi khi nó sai và bạn phải đưa vào một tuyên bố trả lại không cần thiết.

char getChoice() {
    int ch = read();

    if (ch == -1 || ch == 'q') {
        System.exit(0);
    }
    else {
        return (char) ch;
    }

    // Cannot reach here, but still an error.
}

Đó là một sự khác biệt triết học. C và C ++ là các ngôn ngữ dễ tin cậy và dễ tin cậy hơn Java hoặc C # và do đó, một số lỗi trong các ngôn ngữ mới hơn là cảnh báo trong C / C ++ và một số cảnh báo được bỏ qua hoặc tắt theo mặc định.


2
Nếu javac thực sự kiểm tra các đường dẫn mã, nó sẽ không thấy rằng bạn không bao giờ có thể đạt đến điểm đó?
Chris Lutz

3
Trong trường hợp đầu tiên, nó không cung cấp cho bạn tín dụng cho tất cả các trường hợp enum (bạn cần một trường hợp mặc định hoặc trả lại sau khi chuyển đổi), và trong trường hợp thứ hai, nó không biết rằng System.exit()không bao giờ trả lại.
John Kugelman

2
Có vẻ như đơn giản để javac (một trình biên dịch mạnh mẽ khác) biết rằng System.exit()không bao giờ trả về. Tôi đã tra cứu nó ( java.sun.com/j2se/1.4.2/docs/api/java/lang/ mẹo ) và các tài liệu chỉ nói rằng "không bao giờ bình thường trở lại". Tôi tự hỏi điều đó có nghĩa là gì ...
Paul Biggar

@Paul: điều đó có nghĩa là họ không có người tốt. Tất cả các ngôn ngữ khác đều nói "không bao giờ trả lại bình thường" - tức là "không trả về bằng cơ chế trả lại thông thường".
Max Lybbert

1
Tôi chắc chắn thích một trình biên dịch ít nhất đã cảnh báo nếu gặp phải ví dụ đầu tiên đó, bởi vì tính chính xác của logic sẽ bị phá vỡ nếu bất kỳ ai thêm một giá trị mới vào enum. Tôi muốn một trường hợp mặc định phàn nàn lớn và / hoặc bị rơi (có thể sử dụng một xác nhận).
Injektilo

14

Ý bạn là, tại sao chảy ra khỏi phần cuối của hàm trả về giá trị (nghĩa là thoát mà không rõ ràng return) không phải là một lỗi?

Thứ nhất, trong C, một hàm có trả về một cái gì đó có ý nghĩa hay không chỉ quan trọng khi mã thực thi thực sự sử dụng giá trị được trả về. Có lẽ ngôn ngữ không muốn buộc bạn trả lại bất cứ điều gì khi bạn biết rằng bạn sẽ không sử dụng ngôn ngữ này trong hầu hết thời gian.

Thứ hai, rõ ràng đặc tả ngôn ngữ không muốn buộc các tác giả biên dịch phát hiện và xác minh tất cả các đường dẫn điều khiển có thể có sự hiện diện của một bản tường minh return(mặc dù trong nhiều trường hợp điều này không khó thực hiện). Ngoài ra, một số đường dẫn điều khiển có thể dẫn đến các hàm không trả về - đặc điểm thường không được trình biên dịch biết đến. Những con đường như vậy có thể trở thành một nguồn tích cực sai gây phiền nhiễu.

Cũng lưu ý rằng C và C ++ khác nhau trong định nghĩa về hành vi của họ trong trường hợp này. Trong C ++, việc kết thúc hàm trả về giá trị luôn luôn là hành vi không xác định (bất kể kết quả của hàm có được sử dụng bởi mã gọi hay không). Trong C, điều này gây ra hành vi không xác định chỉ khi mã gọi cố gắng sử dụng giá trị được trả về.


+1 nhưng C ++ không thể bỏ qua các returncâu lệnh từ cuối main()?
Chris Lutz

3
@Chris Lutz: Vâng, mainđặc biệt về vấn đề đó.
AnT

5

Theo C / C ++, việc không trả về từ một hàm yêu cầu trả lại một cái gì đó là hợp pháp. Có một số trường hợp sử dụng, chẳng hạn như gọi điện exit(-1), hoặc một chức năng gọi nó hoặc ném ngoại lệ.

Trình biên dịch sẽ không từ chối C ++ hợp pháp ngay cả khi nó dẫn đến UB nếu bạn yêu cầu nó không. Cụ thể, bạn đang yêu cầu không có cảnh báo nào được tạo ra. (Gcc vẫn bật một số theo mặc định, nhưng khi được thêm vào, những thứ đó dường như phù hợp với các tính năng mới không phải là cảnh báo mới cho các tính năng cũ)

Thay đổi gcc no-arg mặc định để phát ra một số cảnh báo có thể là một thay đổi đột phá cho các tập lệnh hiện có hoặc tạo hệ thống. Những cái được thiết kế tốt hoặc -Wallxử lý các cảnh báo, hoặc chuyển các cảnh báo riêng lẻ.

Học cách sử dụng chuỗi công cụ C ++ là một rào cản đối với việc học trở thành lập trình viên C ++, nhưng chuỗi công cụ C ++ thường được viết bởi và dành cho các chuyên gia.


Vâng, trong tôi Makefilecó nó chạy cùng -Wall -Wpedantic -Werror, nhưng đây là một kịch bản thử nghiệm một lần mà tôi quên cung cấp các đối số.
Vụ kiện của Quỹ Monica

2
Ví dụ, làm cho -Wduplicated-condmột phần của -Wallbootstrap GCC bị hỏng. Một số cảnh báo có vẻ phù hợp trong hầu hết các mã là không phù hợp trong tất cả các mã. Đó là lý do tại sao chúng không được bật theo mặc định.
uh oh ai đó cần một học sinh

câu đầu tiên của bạn dường như mâu thuẫn với câu trích dẫn trong câu trả lời được chấp nhận "Chảy ra .... hành vi không xác định ...". Hay ub được coi là "hợp pháp"? Hoặc bạn có nghĩa là nó không phải là UB trừ khi giá trị trả về (không) thực sự được sử dụng? Tôi lo lắng về trường hợp C ++ btw
idclev 463035818

@ tobi303 int foo() { exit(-1); }không trả về inttừ hàm "yêu cầu trả về int". Điều này là hợp pháp trong C ++. Bây giờ, nó không trả lại bất cứ điều gì ; sự kết thúc của chức năng đó không bao giờ đạt được. Trên thực tế đến cuối cùng foosẽ là hành vi không xác định. Bỏ qua các trường hợp kết thúc, int foo() { throw 3.14; }cũng yêu cầu trả lại intnhưng không bao giờ làm.
Yakk - Adam Nevraumont

1
vì vậy tôi đoán void* foo(void* arg) { pthread_exit(NULL); }là ổn vì lý do tương tự (khi cách sử dụng duy nhất là thông qua pthread_create(...,...,foo,...);)
idclev 463035818

1

Tôi tin rằng điều này là do mã kế thừa (C không bao giờ yêu cầu trả về câu lệnh C ++ cũng vậy). Có lẽ có cơ sở mã lớn dựa trên "tính năng" đó. Nhưng ít nhất có -Werror=return-type cờ trên nhiều trình biên dịch (bao gồm gcc và clang).


1

Trong một số trường hợp giới hạn và hiếm gặp, việc kết thúc hàm không trống mà không trả về giá trị có thể hữu ích. Giống như mã dành riêng cho MSVC sau đây:

double pi()
{
    __asm fldpi
}

Hàm này trả về pi bằng cách sử dụng lắp ráp x86. Không giống như lắp ráp trong GCC, tôi biết không có cách nào để sử dụng returnđể làm điều này mà không liên quan đến chi phí trong kết quả.

Theo như tôi biết, trình biên dịch C ++ chính thống nên phát ra ít nhất các cảnh báo cho mã rõ ràng không hợp lệ. Nếu tôi làm cho phần thân pi()trống, GCC / Clang sẽ báo cáo cảnh báo và MSVC sẽ báo lỗi.

Mọi người đề cập đến ngoại lệ và exittrong một số câu trả lời. Những lý do không hợp lệ. Hoặc ném một ngoại lệ, hoặc gọi exit, sẽ không làm cho việc thực thi chức năng kết thúc. Và các trình biên dịch biết điều đó: viết một câu lệnh ném hoặc gọi exittrong phần thân trống pi()sẽ dừng mọi cảnh báo hoặc lỗi từ trình biên dịch.


MSVC đặc biệt hỗ trợ rơi khỏi sự kết thúc của một voidchức năng sau khi asm nội tuyến để lại một giá trị trong thanh ghi giá trị trả về. (x87 st0trong trường hợp này, EAX cho số nguyên. Và có thể xmm0 trong quy ước gọi trả về float / double trong xmm0 thay vì st0). Xác định hành vi này là cụ thể đối với MSVC; thậm chí không kêu -fasm-blocksđể hỗ trợ cú pháp tương tự làm cho điều này an toàn. Xem Có __asm ​​{}; Trả về giá trị của eax?
Peter Cordes

0

Trong hoàn cảnh nào nó không tạo ra lỗi? Nếu nó khai báo một kiểu trả về và không trả về một cái gì đó, thì nó có vẻ như là một lỗi đối với tôi.

Một ngoại lệ tôi có thể nghĩ đến là main()hàm, không cần một returncâu lệnh nào cả (ít nhất là trong C ++; tôi không có một trong các tiêu chuẩn C tiện dụng). Nếu không có sự trở lại, nó sẽ hoạt động như thể return 0;là tuyên bố cuối cùng.


1
main()cần một returnC.
Chris Lutz

@Jefromi: OP đang hỏi về chức năng không trống mà không có return <value>;tuyên bố
Bill Bill

2
chính tự động trả về 0 trong C và C ++. C89 cần một sự trở lại rõ ràng.
Julian Schaub - litb

1
@Chris: trong C99 có một ẩn return 0;ở cuối main()(và main()chỉ). Nhưng dù sao nó cũng là phong cách tốt để thêm vào return 0;.
PMG

0

Âm thanh như bạn cần để cảnh báo trình biên dịch của bạn:

$ gcc -Wall -Wextra -Werror -x c -
int main(void) { return; }
cc1: warnings being treated as errors
<stdin>: In function main’:
<stdin>:1: warning: return with no value, in function returning non-void
<stdin>:1: warning: control reaches end of non-void function
$

5
Nói "bật -Werror" là không trả lời. Rõ ràng có một sự khác biệt về mức độ nghiêm trọng giữa các vấn đề được phân loại là cảnh báo và lỗi, và gcc coi vấn đề này là lớp ít nghiêm trọng hơn.
Cascabel

2
@Jefromi: Từ quan điểm ngôn ngữ thuần túy, không có sự khác biệt giữa các cảnh báo và lỗi. Trình biên dịch chỉ được yêu cầu để đưa ra một "thông điệp không nhận thức". Không có yêu cầu dừng biên dịch hoặc gọi một cái gì đó là "eror" và một cái gì đó khác là "một cảnh báo". Một thông điệp chẩn đoán được đưa ra (hoặc bất kỳ loại nào), hoàn toàn tùy thuộc vào bạn để đưa ra quyết định.
AnT

1
Sau đó, một lần nữa, vấn đề trong câu hỏi gây ra UB. Trình biên dịch không bắt buộc phải bắt UB cả.
AnT

1
Trong 6.9.1 / 12 trong n1256, nó nói "Nếu} kết thúc một hàm được đạt tới và giá trị của lệnh gọi hàm được sử dụng bởi người gọi, hành vi không được xác định."
Julian Schaub - litb

2
@Chris Lutz: Tôi không thấy nó. Đó là một vi phạm ràng buộc để sử dụng một khoảng trống rõ ràngreturn; trong một hàm không trống và đó là một vi phạm ràng buộc để sử dụng return <value>;trong một hàm void. Nhưng đó là, tôi tin, không phải là chủ đề. OP, như tôi đã hiểu, là về việc thoát khỏi một hàm không trống mà không có return(chỉ cho phép điều khiển chảy ra khỏi phần cuối của hàm). Đây không phải là vi phạm ràng buộc, AFAIK. Tiêu chuẩn chỉ nói rằng nó luôn là UB trong C ++ và đôi khi là UB ở C.
AnT

-2

Đó là một vi phạm ràng buộc trong c99, nhưng không phải trong c89. Tương phản:

c89:

3.6.6.4 returnTuyên bố

Những ràng buộc

Một returncâu lệnh với một biểu thức sẽ không xuất hiện trong một hàm có kiểu trả về là void.

c99:

6.8.6.4 returnTuyên bố

Những ràng buộc

Một returncâu lệnh với một biểu thức sẽ không xuất hiện trong một hàm có kiểu trả về là void. Một returncâu lệnh không có biểu thức sẽ chỉ xuất hiện trong một hàm có kiểu trả về là void.

Ngay cả trong --std=c99chế độ, gcc sẽ chỉ đưa ra cảnh báo (mặc dù không cần bật -Wcờ bổ sung , theo yêu cầu theo mặc định hoặc theo c89 / 90).

Chỉnh sửa để thêm vào đó trong c89, "đạt đến }chức năng chấm dứt chức năng tương đương với thực thi một returncâu lệnh mà không có biểu thức" (3.6.6.4). Tuy nhiên, trong c99, hành vi không được xác định (6.9.1).


4
Lưu ý rằng điều này chỉ bao gồm các returntuyên bố rõ ràng . Nó không bao gồm rơi ra khỏi phần cuối của hàm mà không trả về giá trị.
John Kugelman

3
Lưu ý rằng C99 bỏ lỡ "đạt tới} chấm dứt chức năng tương đương với thực thi câu lệnh trả về mà không có biểu thức" vì vậy nó không vi phạm ràng buộc và do đó không cần chẩn đoán.
Julian Schaub - litb
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.