Mengapa mengalir dari fungsi non-void tanpa mengembalikan nilai tidak menghasilkan kesalahan kompiler?

158

Sejak saya menyadari beberapa tahun yang lalu, bahwa ini tidak menghasilkan kesalahan secara default (setidaknya di GCC), saya selalu bertanya-tanya mengapa?

Saya mengerti bahwa Anda dapat mengeluarkan flag compiler untuk menghasilkan peringatan, tetapi bukankah itu selalu menjadi kesalahan? Mengapa masuk akal jika fungsi non-void tidak mengembalikan nilai yang valid?

Contoh seperti yang diminta dalam komentar:

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

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

... mengkompilasi.

Catskul
sumber
9
Atau, saya memperlakukan semua peringatan namun kesalahan seperti sepele, dan saya mengaktifkan semua peringatan yang saya bisa (dengan penonaktifan lokal jika perlu ... tapi kemudian jelas dalam kode mengapa).
Matthieu M.
8
-Werror=return-typeakan memperlakukan peringatan itu hanya sebagai kesalahan. Saya hanya mengabaikan peringatan dan beberapa menit frustrasi melacak thispointer yang tidak valid membawa saya ke sini dan sampai pada kesimpulan ini.
jozxyqk
Ini diperburuk oleh kenyataan bahwa mengalir dari akhir suatu std::optionalfungsi tanpa mengembalikan mengembalikan opsional "benar"
Rufus
@Rufus Tidak harus. Itulah yang terjadi pada siklus mesin / compiler / OS / lunar Anda. Apapun kode sampah yang dihasilkan oleh kompiler karena perilaku yang tidak terdefinisi hanya terlihat seperti pilihan 'benar', apa pun itu.
underscore_d

Jawaban:

146

Standar C99 dan C ++ tidak memerlukan fungsi untuk mengembalikan nilai. Pernyataan pengembalian yang hilang dalam fungsi pengembalian nilai akan didefinisikan (untuk kembali 0) hanya dalam mainfungsi.

Alasannya mencakup memeriksa apakah setiap jalur kode mengembalikan nilai yang cukup sulit, dan nilai balik dapat diatur dengan assembler tertanam atau metode rumit lainnya.

Dari konsep C ++ 11 :

§ 6.6.3 / 2

Mengalir dari akhir fungsi [...] menghasilkan perilaku yang tidak terdefinisi dalam fungsi pengembalian nilai.

§ 3.6.1 / 5

Jika kontrol mencapai akhir maintanpa menemukan return pernyataan, efeknya adalah menjalankan

return 0;

Perhatikan bahwa perilaku yang dijelaskan dalam C ++ 6.6.3 / 2 tidak sama dengan C.


gcc akan memberi Anda peringatan jika Anda menyebutnya dengan opsi -Wreturn-type.

-Wreturn-type Peringatkan setiap fungsi didefinisikan dengan tipe-kembali yang default ke int. Juga memperingatkan tentang pernyataan kembali apa pun tanpa nilai kembali dalam fungsi yang tipe pengembaliannya tidak batal (jatuh dari ujung badan fungsi dianggap kembali tanpa nilai), dan tentang pernyataan balik dengan ekspresi dalam fungsi yang tipe kembali tidak valid.

Peringatan ini diaktifkan oleh -Wall .


Sama seperti rasa ingin tahu, lihat apa yang dilakukan kode ini:

#include <iostream>

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

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

Kode ini secara formal memiliki perilaku yang tidak terdefinisi, dan dalam praktiknya itu memanggil konvensi dan arsitektur bergantung. Pada satu sistem tertentu, dengan satu kompiler tertentu, nilai kembali adalah hasil dari evaluasi ekspresi terakhir, disimpan dalam eaxregister prosesor sistem itu.

fnieto - Fernando Nieto
sumber
13
Saya akan waspada menyebut perilaku yang tidak terdefinisi "diizinkan", meskipun harus diakui saya juga salah menyebutnya "dilarang". Tidak menjadi kesalahan dan tidak memerlukan diagnostik tidak sama dengan "diizinkan". Paling tidak, jawaban Anda berbunyi seperti Anda mengatakan itu boleh dilakukan, yang sebagian besar tidak.
Lightness Races in Orbit
4
@Catskul, mengapa Anda membeli argumen itu? Tidakkah layak, jika tidak mudah, untuk mengidentifikasi titik keluar dari suatu fungsi dan memastikan semuanya mengembalikan nilai (dan nilai dari tipe pengembalian yang dinyatakan)?
BlueBomber
3
@Catskul, ya dan tidak. Bahasa yang diketik dan / atau dikompilasi secara statis melakukan banyak hal yang mungkin Anda anggap "sangat mahal", tetapi karena mereka hanya melakukannya sekali, pada waktu kompilasi, mereka memiliki biaya yang sangat kecil. Bahkan setelah mengatakan itu, saya tidak melihat mengapa mengidentifikasi titik keluar dari suatu fungsi harus super linier: Anda hanya melintasi AST dari fungsi dan mencari panggilan kembali atau keluar. Itu waktu linier, yang jelas-jelas efisien.
BlueBomber
3
@LightnessRacesinOrbit: Jika suatu fungsi dengan nilai kembali kadang-kadang kembali segera dengan nilai dan kadang-kadang memanggil fungsi lain yang selalu keluar melalui throwatau longjmp, haruskah kompiler memerlukan panggilan yang tidak terjangkau returnsetelah panggilan ke fungsi yang tidak kembali? Kasus di mana itu tidak diperlukan tidak terlalu umum, dan persyaratan untuk memasukkannya bahkan dalam kasus seperti itu mungkin tidak akan berat, tetapi keputusan untuk tidak mengharuskannya masuk akal.
supercat
1
@supercat: Kompiler super-cerdas tidak akan memperingatkan atau kesalahan dalam kasus seperti itu, tetapi - sekali lagi - ini pada dasarnya tidak terhitung untuk kasus umum, jadi Anda terjebak dengan aturan umum. Namun, jika Anda tahu bahwa fungsi akhir Anda tidak akan pernah tercapai, maka Anda jauh dari semantik penanganan fungsi tradisional itu, ya, Anda dapat melanjutkan dan melakukan ini dan tahu bahwa itu aman. Terus terang, Anda adalah lapisan di bawah C ++ pada saat itu dan, dengan demikian, semua jaminannya masih bisa diperdebatkan.
Lightness Races in Orbit
42

gcc tidak secara default memeriksa bahwa semua jalur kode mengembalikan nilai karena secara umum ini tidak dapat dilakukan. Ini mengasumsikan Anda tahu apa yang Anda lakukan. Pertimbangkan contoh umum menggunakan enumerasi:

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

    // Error, no return?
}

Anda pemrogram tahu bahwa, kecuali bug, metode ini selalu mengembalikan warna. gcc percaya bahwa Anda tahu apa yang Anda lakukan sehingga tidak memaksa Anda untuk mengembalikan fungsi.

javac, di sisi lain, mencoba memverifikasi bahwa semua jalur kode mengembalikan nilai dan melemparkan kesalahan jika tidak dapat membuktikan bahwa mereka semua melakukannya. Kesalahan ini diamanatkan oleh spesifikasi bahasa Java. Perhatikan bahwa kadang-kadang itu salah dan Anda harus memasukkan pernyataan pengembalian yang tidak perlu.

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

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

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

Ini perbedaan filosofis. C dan C ++ lebih permisif dan lebih bisa dipercaya daripada bahasa Java atau C # sehingga beberapa kesalahan dalam bahasa yang lebih baru adalah peringatan di C / C ++ dan beberapa peringatan diabaikan atau dinonaktifkan secara default.

John Kugelman
sumber
2
Jika javac benar-benar memeriksa jalur kode, tidakkah itu akan melihat bahwa Anda tidak pernah bisa mencapai titik itu?
Chris Lutz
3
Dalam yang pertama itu tidak memberi Anda kredit untuk menutupi semua kasus enum (Anda membutuhkan kasus default atau pengembalian setelah saklar), dan yang kedua tidak tahu bahwa System.exit()tidak pernah kembali.
John Kugelman
2
Tampaknya mudah bagi javac (kompiler yang kuat) untuk mengetahui bahwa System.exit()tidak pernah kembali. Saya mencarinya ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), dan dokumen hanya mengatakan itu "tidak pernah biasanya kembali". Saya ingin tahu apa artinya itu ... #
Paul Biggar
@ Paul: itu berarti mereka tidak memiliki goodeditor. Semua bahasa lain mengatakan "tidak pernah kembali secara normal" - yaitu, "tidak kembali menggunakan mekanisme pengembalian normal."
Max Lybbert
1
Saya pasti lebih suka kompiler yang setidaknya memperingatkan jika menemukan contoh pertama itu, karena kebenaran logika akan pecah jika ada yang menambahkan nilai baru ke enum. Saya ingin kasus default yang mengeluh keras dan / atau jatuh (mungkin menggunakan pernyataan).
Injektilo
14

Maksud Anda, mengapa mengalir dari akhir fungsi pengembalian nilai (yaitu keluar tanpa eksplisit return) bukan kesalahan?

Pertama, di C apakah suatu fungsi mengembalikan sesuatu yang bermakna atau tidak hanya penting ketika kode yang mengeksekusi benar-benar menggunakan nilai yang dikembalikan. Mungkin bahasa tidak ingin memaksa Anda untuk mengembalikan apa pun ketika Anda tahu bahwa Anda tidak akan menggunakannya lagi sebagian besar waktu.

Kedua, ternyata spesifikasi bahasa tidak ingin memaksa penulis kompiler untuk mendeteksi dan memverifikasi semua jalur kontrol yang mungkin untuk keberadaan eksplisit return(walaupun dalam banyak kasus ini tidak terlalu sulit untuk dilakukan). Juga, beberapa jalur kontrol mungkin mengarah ke fungsi yang tidak kembali - sifat yang umumnya tidak diketahui oleh kompiler. Jalan seperti itu bisa menjadi sumber positif palsu yang menjengkelkan.

Perhatikan juga, bahwa C dan C ++ berbeda dalam definisi perilaku dalam kasus ini. Dalam C + + hanya mengalir dari akhir fungsi nilai yang kembali selalu perilaku tidak terdefinisi (terlepas dari apakah hasil fungsi digunakan oleh kode panggilan). Dalam C ini menyebabkan perilaku yang tidak terdefinisi hanya jika kode panggilan mencoba menggunakan nilai yang dikembalikan.

Semut
sumber
+1 tetapi tidak dapatkah C ++ menghilangkan returnpernyataan dari akhir main()?
Chris Lutz
3
@ Chris Lutz: Ya, mainspesial dalam hal itu.
AnT
5

Adalah legal di bawah C / C ++ untuk tidak kembali dari fungsi yang mengklaim mengembalikan sesuatu. Ada sejumlah kasus penggunaan, seperti panggilan exit(-1), atau fungsi yang memanggilnya atau melempar pengecualian.

Kompiler tidak akan menolak C ++ legal meskipun itu mengarah ke UB jika Anda memintanya. Secara khusus, Anda meminta tidak ada peringatan yang dihasilkan. (Gcc masih mengaktifkan beberapa secara default, tetapi ketika ditambahkan mereka tampaknya sejajar dengan fitur baru, bukan peringatan baru untuk fitur lama)

Mengubah no-arg gcc default untuk memancarkan beberapa peringatan bisa menjadi perubahan besar untuk skrip yang ada atau membuat sistem. Yang dirancang dengan baik baik -Walldan menangani peringatan, atau beralih peringatan individu.

Belajar menggunakan rantai alat C ++ adalah penghalang untuk belajar menjadi programmer C ++, tetapi rantai alat C ++ biasanya ditulis oleh dan untuk para ahli.

Yakk - Adam Nevraumont
sumber
Ya, di saya Makefilesaya harus berjalan dengan -Wall -Wpedantic -Werror, tapi ini adalah satu-off tes script yang saya lupa untuk memasok argumen untuk.
Dana Gugatan Monica
2
Sebagai contoh, membuat -Wduplicated-condbagian dari -Wallbootstrap GCC yang rusak. Beberapa peringatan yang tampaknya sesuai di sebagian besar kode tidak sesuai di semua kode. Itu sebabnya mereka tidak diaktifkan secara default.
uh oh seseorang butuh anak anjing
kalimat pertama Anda tampaknya bertentangan dengan kutipan dalam jawaban yang diterima "Mengalir .... perilaku yang tidak terdefinisi ...". Atau apakah ub dianggap "legal"? Atau maksud Anda bahwa ini bukan UB kecuali nilai yang dikembalikan tidak digunakan? Saya khawatir tentang kasus C ++ btw
idclev 463035818
@ tobi303 int foo() { exit(-1); }tidak kembali intdari fungsi yang "mengklaim untuk mengembalikan int". Ini legal di C ++. Sekarang, itu tidak mengembalikan apa pun ; akhir dari fungsi itu tidak pernah tercapai. Sebenarnya mencapai akhir fooakan menjadi perilaku yang tidak terdefinisi. Mengabaikan akhir dari proses kasus, int foo() { throw 3.14; }juga mengklaim kembali inttetapi tidak pernah melakukannya.
Yakk - Adam Nevraumont
1
jadi saya kira void* foo(void* arg) { pthread_exit(NULL); }baik-baik saja untuk alasan yang sama (ketika penggunaannya hanya melalui pthread_create(...,...,foo,...);)
idclev 463035818
1

Saya percaya ini karena kode warisan (C tidak pernah meminta pernyataan pengembalian begitu pula C ++). Mungkin ada basis kode besar bergantung pada "fitur" itu. Tetapi setidaknya ada -Werror=return-type flag pada banyak compiler (termasuk gcc dan dentang).

d.lozinski
sumber
1

Dalam beberapa kasus terbatas dan langka, mengalir dari ujung fungsi yang tidak kosong tanpa mengembalikan nilai bisa berguna. Seperti kode spesifik MSVC berikut:

double pi()
{
    __asm fldpi
}

Fungsi ini mengembalikan pi menggunakan rakitan x86. Tidak seperti perakitan di GCC, saya tahu tidak ada cara returnuntuk melakukan ini tanpa melibatkan overhead dalam hasilnya.

Sejauh yang saya tahu, kompiler C ++ mainstream harus memancarkan setidaknya peringatan untuk kode yang tampaknya tidak valid. Jika saya membuat badan pi()kosong, GCC / Dentang akan melaporkan peringatan, dan MSVC akan melaporkan kesalahan.

Orang menyebutkan pengecualian dan exitdalam beberapa jawaban. Itu bukan alasan yang valid. Entah melempar pengecualian, atau memanggil exit, tidak akan membuat fungsi eksekusi mengalir pada akhirnya. Dan kompiler tahu itu: menulis pernyataan melempar atau memanggil exitdalam tubuh kosong pi()akan menghentikan peringatan atau kesalahan dari kompiler.

Yongwei Wu
sumber
MSVC secara khusus mendukung jatuh dari non- voidfungsi setelah inline asm meninggalkan nilai dalam register nilai-kembali. (x87 st0dalam hal ini, EAX untuk integer. Dan mungkin xmm0 dalam konvensi pemanggilan yang mengembalikan float / double di xmm0 bukannya st0). Mendefinisikan perilaku ini khusus untuk MSVC; bahkan tidak bergantung pada -fasm-blocksuntuk mendukung sintaks yang sama membuat ini aman. Lihat Apakah __asme {}; mengembalikan nilai eax?
Peter Cordes
0

Dalam keadaan apa itu tidak menghasilkan kesalahan? Jika ini menyatakan tipe kembali dan tidak mengembalikan sesuatu, itu terdengar seperti kesalahan bagi saya.

Satu-satunya pengecualian yang dapat saya pikirkan adalah main()fungsi, yang tidak memerlukan returnpernyataan sama sekali (setidaknya dalam C ++; Saya tidak memiliki salah satu standar C yang berguna). Jika tidak ada pengembalian, itu akan bertindak seolah-olah return 0;adalah pernyataan terakhir.

David Thornley
sumber
1
main()membutuhkan nilai returnC.
Chris Lutz
@Jefromi: OP bertanya tentang fungsi non-void tanpa return <value>;pernyataan
Bill
2
main secara otomatis mengembalikan 0 dalam C dan C ++. C89 membutuhkan pengembalian eksplisit.
Johannes Schaub - litb
1
@ Chris: di C99 ada yang tersirat return 0;di akhir main()(dan main()hanya). Tapi return 0;toh itu gaya yang bagus untuk ditambahkan .
pmg
0

Sepertinya Anda perlu mengaktifkan peringatan kompiler Anda:

$ 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
$
Chris Lutz
sumber
5
Mengatakan "nyalakan -Werror" adalah non-jawaban. Jelas ada perbedaan tingkat keparahan antara masalah yang diklasifikasikan sebagai peringatan dan kesalahan, dan gcc memperlakukan yang ini sebagai kelas yang tidak terlalu parah.
Cascabel
2
@Jefromi: Dari sudut pandang bahasa murni, tidak ada perbedaan antara peringatan dan kesalahan. Kompiler hanya diminta untuk mengeluarkan "pesan disgnostik". Tidak ada persyaratan untuk menghentikan kompilasi atau menyebut sesuatu "eror" dan sesuatu yang lain "peringatan". Satu pesan diagnostik dikeluarkan (atau apa pun jenisnya), sepenuhnya terserah Anda untuk mengambil keputusan.
AnT
1
Kemudian lagi, masalah tersebut menyebabkan UB. Compiler tidak diharuskan untuk menangkap UB sama sekali.
AnT
1
Dalam 6.9.1 / 12 dalam n1256 dikatakan "Jika} yang mengakhiri suatu fungsi tercapai, dan nilai dari pemanggilan fungsi digunakan oleh pemanggil, perilaku tidak terdefinisi."
Johannes Schaub - litb
2
@ Chris Lutz: Saya tidak melihatnya. Ini adalah pelanggaran kendala untuk menggunakan kosong eksplisitreturn; dalam fungsi non-void, dan ini merupakan pelanggaran kendala untuk digunakan return <value>;dalam fungsi void. Tapi itu, saya percaya, bukan topiknya. OP, seperti yang saya pahami, adalah tentang keluar dari fungsi non-void tanpa return(hanya memungkinkan kontrol mengalir dari akhir fungsi). Ini bukan pelanggaran kendala, AFAIK. Standar hanya mengatakan itu selalu UB dalam C ++ dan kadang-kadang UB dalam C.
AnT
-2

Ini adalah pelanggaran kendala pada c99, tetapi tidak pada c89. Kontras:

c89:

3.6.6.4 returnPernyataan

Kendala

Sebuah returnpernyataan dengan ekspresi tidak akan muncul dalam fungsi yang jenis kembali void.

c99:

6.8.6.4 returnPernyataan

Kendala

Sebuah returnpernyataan dengan ekspresi tidak akan muncul dalam fungsi yang jenis kembali void. Sebuah returnpernyataan tanpa ekspresi akan hanya muncul dalam fungsi yang jenis kembali void.

Bahkan dalam --std=c99mode, gcc hanya akan memberikan peringatan (meskipun tanpa perlu mengaktifkan -Wflag tambahan , seperti yang diharuskan secara default atau dalam c89 / 90).

Edit untuk menambahkan bahwa di c89, "mencapai }yang mengakhiri fungsi sama dengan mengeksekusi returnpernyataan tanpa ekspresi" (3.6.6.4). Namun, dalam c99 perilaku tidak terdefinisi (6.9.1).

Matt B.
sumber
4
Perhatikan bahwa ini hanya mencakup returnpernyataan eksplisit . Itu tidak mencakup jatuh dari akhir fungsi tanpa mengembalikan nilai.
John Kugelman
3
Perhatikan bahwa C99 melewatkan "mencapai} yang mengakhiri suatu fungsi sama dengan mengeksekusi pernyataan kembali tanpa ekspresi" jadi itu tidak membuat pelanggaran kendala, dan dengan demikian tidak diperlukan diagnostik.
Johannes Schaub - litb