Pertanyaan Mengapa saya tidak bisa mendefinisikan fungsi di dalam fungsi lain?


Ini bukan pertanyaan fungsi lambda, saya tahu bahwa saya dapat menetapkan lambda ke variabel.

Apa gunanya mengizinkan kami untuk menyatakan, tetapi tidak mendefinisikan fungsi di dalam kode?

Sebagai contoh:

#include <iostream>

int main()
{
    // This is illegal
    // int one(int bar) { return 13 + bar; }

    // This is legal, but why would I want this?
    int two(int bar);

    // This gets the job done but man it's complicated
    class three{
        int m_iBar;
    public:
        three(int bar):m_iBar(13 + bar){}
        operator int(){return m_iBar;}
    }; 

    std::cout << three(42) << '\n';
    return 0;
}

Jadi yang ingin saya ketahui adalah mengapa C ++ memungkinkan two yang tampaknya tidak berguna, dan three yang tampaknya jauh lebih rumit, tetapi tidak diizinkan one?

EDIT:

Dari jawaban tampaknya ada deklarasi kode dapat mencegah pencemaran namespace, apa yang saya harap dapat saya dengar adalah mengapa kemampuan mendeklarasikan fungsi telah diizinkan tetapi kemampuan untuk mendefinisikan fungsi telah dianulir.


75
2018-04-30 11:58


asal


Jawaban:


Tidak jelas mengapa one tidak diizinkan; fungsi bersarang diusulkan sejak lama N0295 yang mengatakan:

Kami membahas pengenalan fungsi yang disatukan ke C ++. Bersarang   fungsinya dipahami dengan baik dan pengenalannya membutuhkan sedikit   upaya baik dari vendor kompilator, programer, atau panitia.   Fungsi tersarang menawarkan keuntungan yang signifikan, [...]

Tentunya proposal ini ditolak, tetapi karena kami tidak memiliki rapat yang tersedia untuk online 1993 kami tidak memiliki sumber yang mungkin untuk alasan penolakan ini.

Sebenarnya proposal ini dicatat dalam Lambda ekspresi dan penutupan untuk C ++ sebagai alternatif yang mungkin:

Satu artikel [Bre88] dan proposal N0295 ke C   ++ komite [SH93] menyarankan untuk menambahkan fungsi yang bersarang ke C   ++. Fungsi bersarang mirip dengan ekspresi lambda, tetapi didefinisikan sebagai pernyataan dalam tubuh fungsi, dan hasilnya   penutupan tidak dapat digunakan kecuali fungsi itu aktif. Proposal ini   juga tidak termasuk menambahkan tipe baru untuk setiap ekspresi lambda, tetapi   alih-alih menerapkannya lebih seperti fungsi normal, termasuk   memungkinkan jenis pointer fungsi khusus untuk merujuk pada mereka. Keduanya   proposal ini mendahului penambahan template ke C   ++, dan tidak menyebutkan penggunaan fungsi bersarang dalam kombinasi dengan algoritma generik. Juga, proposal ini tidak memiliki cara untuk menyalin   variabel lokal menjadi penutupan, sehingga fungsi bertingkat mereka   menghasilkan benar-benar tidak dapat digunakan di luar fungsi melampirkan mereka

Mengingat kita sekarang memiliki lambdas, kita tidak mungkin melihat fungsi bertingkat karena, seperti diuraikan di atas kertas, mereka adalah alternatif untuk masalah yang sama dan fungsi bersarang memiliki beberapa keterbatasan relatif terhadap lambdas.

Adapun bagian dari pertanyaan Anda ini:

// This is legal, but why would I want this?
int two(int bar);

Ada beberapa kasus di mana ini akan menjadi cara yang berguna untuk memanggil fungsi yang Anda inginkan. Rancangan bagian standar C ++ 3.4.1  [basic.lookup.unqual] memberi kita satu contoh yang menarik:

namespace NS {
    class T { };
    void f(T);
    void g(T, int);
}

NS::T parm;
void g(NS::T, float);

int main() {
    f(parm); // OK: calls NS::f
    extern void g(NS::T, float);
    g(parm, 1); // OK: calls g(NS::T, float)
}

39
2018-04-30 14:30



Jawabannya adalah "alasan historis". Di C Anda bisa memiliki deklarasi fungsi di lingkup blok, dan desainer C ++ tidak melihat manfaat dalam menghapus opsi itu.

Contoh penggunaannya adalah:

#include <iostream>

int main()
{
    int func();
    func();
}

int func()
{
    std::cout << "Hello\n";
}

IMO ini adalah ide yang buruk karena mudah untuk membuat kesalahan dengan memberikan deklarasi yang tidak sesuai dengan definisi sebenarnya fungsi, yang mengarah ke perilaku tidak terdefinisi yang tidak akan didiagnosis oleh compiler.


30
2018-04-30 12:13



Dalam contoh yang Anda berikan, void two(int) sedang dinyatakan sebagai fungsi eksternal, dengan deklarasi itu hanya berlaku dalam ruang lingkup main fungsi.

Itu wajar jika Anda hanya ingin membuat nama two tersedia dalam main() sehingga untuk menghindari polusi namespace global dalam unit kompilasi saat ini.

Contoh dalam menanggapi komentar:

main.cpp:

int main() {
  int foo();
  return foo();
}

foo.cpp:

int foo() {
  return 0;
}

tidak perlu file header. kompilasi dan tautan dengan

c++ main.cpp foo.cpp 

itu akan mengkompilasi dan menjalankan, dan program akan mengembalikan 0 seperti yang diharapkan.


21
2018-04-30 12:18



Anda dapat melakukan hal-hal ini, terutama karena mereka sebenarnya tidak terlalu sulit untuk dilakukan.

Dari sudut pandang compiler, memiliki deklarasi fungsi di dalam fungsi lain cukup sepele untuk diterapkan. Compiler memerlukan mekanisme untuk memungkinkan deklarasi di dalam fungsi untuk menangani deklarasi lain (misalnya, int x;) di dalam fungsi pula.

Biasanya akan memiliki mekanisme umum untuk menguraikan deklarasi. Untuk orang yang menulis compiler, tidak masalah sama sekali apakah mekanisme itu dipanggil ketika mengurai kode di dalam atau di luar fungsi lain - itu hanya sebuah deklarasi, jadi ketika ia melihat cukup untuk mengetahui bahwa apa yang ada adalah deklarasi, ia memanggil bagian dari kompiler yang berhubungan dengan deklarasi.

Bahkan, melarang deklarasi khusus ini di dalam fungsi mungkin akan menambah kerumitan ekstra, karena kompiler akan membutuhkan pemeriksaan yang sepenuhnya serampangan untuk melihat apakah itu sudah melihat kode di dalam definisi fungsi dan berdasarkan yang memutuskan apakah akan mengizinkan atau melarang hal ini. pernyataan.

Itu meninggalkan pertanyaan tentang bagaimana fungsi bertingkat berbeda. Fungsi bertingkat berbeda karena bagaimana hal itu memengaruhi pembuatan kode. Dalam bahasa yang memungkinkan fungsi bertingkat (mis., Pascal) Anda biasanya berharap bahwa kode dalam fungsi bertingkat memiliki akses langsung ke variabel fungsi di mana ia bersarang. Sebagai contoh:

int foo() { 
    int x;

    int bar() { 
        x = 1; // Should assign to the `x` defined in `foo`.
    }
}

Tanpa fungsi lokal, kode untuk mengakses variabel lokal cukup sederhana. Dalam implementasi khas, ketika eksekusi memasuki fungsi, beberapa blok ruang untuk variabel lokal dialokasikan pada stack. Semua variabel lokal dialokasikan dalam satu blok itu, dan setiap variabel diperlakukan hanya sebagai offset dari awal (atau akhir) dari blok tersebut. Sebagai contoh, mari kita mempertimbangkan fungsi seperti ini:

int f() { 
   int x;
   int y;
   x = 1;
   y = x;
   return y;
}

Compiler (dengan asumsi itu tidak mengoptimalkan jauh kode tambahan) mungkin menghasilkan kode untuk ini kira-kira setara dengan ini:

stack_pointer -= 2 * sizeof(int);      // allocate space for local variables
x_offset = 0;
y_offset = sizeof(int);

stack_pointer[x_offset] = 1;                           // x = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];     // y = x;
return_location = stack_pointer[y_offset];             // return y;
stack_pointer += 2 * sizeof(int);

Secara khusus, itu satu lokasi menunjuk ke awal blok variabel lokal, dan semua akses ke variabel lokal adalah sebagai offset dari lokasi itu.

Dengan fungsi bertumpuk, itu bukan lagi kasus - sebaliknya, fungsi memiliki akses tidak hanya ke variabel lokalnya sendiri, tetapi juga ke variabel lokal ke semua fungsi di mana ia bersarang. Alih-alih hanya memiliki satu "stack_pointer" dari mana ia menghitung offset, perlu berjalan kembali ke tumpukan untuk menemukan stack_pointers lokal ke fungsi-fungsi di mana itu bersarang.

Sekarang, dalam kasus sepele itu tidak terlalu mengerikan - jika bar bersarang di dalam foo, kemudian bar hanya bisa mencari tumpukan di pointer stack sebelumnya untuk mengakses foovariabel. Kanan?

Salah! Nah, ada beberapa kasus di mana ini bisa benar, tetapi belum tentu demikian. Khususnya, bar bisa rekursif, dalam hal ini suatu doa yang diberikan bar mungkin harus melihat beberapa tingkat yang hampir acak menaikkan cadangan untuk menemukan variabel fungsi di sekitarnya. Secara umum, Anda perlu melakukan salah satu dari dua hal: apakah Anda memasukkan beberapa data tambahan pada tumpukan, sehingga dapat mencari cadangan tumpukan pada saat run-time untuk menemukan bingkai tumpukan fungsi di sekitarnya, atau jika tidak, Anda secara efektif meneruskan pointer ke bingkai tumpukan fungsi di sekitarnya sebagai parameter tersembunyi untuk fungsi bertingkat. Oh, tetapi tidak selalu ada satu fungsi di sekitarnya - jika Anda dapat melakukan fungsi sarang, Anda mungkin dapat menumpuknya (kurang lebih) secara sewenang-wenang, sehingga Anda harus siap untuk melewati sejumlah parameter tersembunyi yang acak. Itu berarti Anda biasanya berakhir dengan sesuatu seperti daftar terkait tumpukan frame ke fungsi-fungsi sekitarnya, dan akses ke variabel fungsi sekitarnya dilakukan dengan berjalan yang daftar terkait untuk menemukan penunjuk tumpukan, kemudian mengakses offset dari penunjuk tumpukan itu.

Itu, bagaimanapun, berarti akses ke variabel "lokal" mungkin bukan masalah sepele. Menemukan kerangka tumpukan yang benar untuk mengakses variabel dapat menjadi tidak sepele, sehingga akses ke variabel fungsi di sekitarnya juga (setidaknya biasanya) lebih lambat daripada akses ke variabel yang benar-benar lokal. Dan, tentu saja, compiler harus menghasilkan kode untuk menemukan frame stack yang tepat, mengakses variabel melalui sembarang jumlah tumpukan frame, dan seterusnya.

Ini adalah kerumitan yang dihindari oleh C dengan melarang fungsi bertumpuk. Sekarang, memang benar bahwa kompiler C ++ saat ini adalah jenis binatang yang agak berbeda dari kompiler C tahun 1970-an. Dengan hal-hal seperti multiple, virtual inheritance, compiler C ++ harus berurusan dengan hal-hal pada sifat umum yang sama dalam hal apapun (yaitu, mencari lokasi variabel kelas dasar dalam kasus-kasus seperti itu dapat menjadi non-sepele juga). Pada basis persentase, mendukung fungsi bertingkat tidak akan menambah banyak kerumitan ke kompiler C ++ saat ini (dan beberapa, seperti gcc, sudah mendukungnya).

Pada saat yang sama, itu jarang menambah banyak utilitas baik. Khususnya, jika Anda ingin mendefinisikan sesuatu itu tindakan seperti fungsi di dalam suatu fungsi, Anda dapat menggunakan ekspresi lambda. Yang benar-benar dibuat ini adalah objek (yaitu, instance dari beberapa kelas) yang memberatkan operator panggilan fungsi (operator()) tetapi masih memberikan kemampuan seperti fungsi. Itu membuat menangkap (atau tidak) data dari konteks sekitarnya lebih eksplisit sekalipun, yang memungkinkan untuk menggunakan mekanisme yang ada daripada menciptakan mekanisme baru dan seperangkat aturan untuk penggunaannya.

Intinya: meskipun awalnya mungkin tampak seperti deklarasi bertingkat sulit dan fungsi bertingkat adalah sepele, lebih atau kurang kebalikannya benar: fungsi bertingkat sebenarnya jauh lebih kompleks untuk mendukung daripada deklarasi bertingkat.


18
2018-04-30 13:13



Yang pertama adalah definisi fungsi, dan itu tidak diperbolehkan. Jelas, adalah penggunaan meletakkan definisi fungsi di dalam fungsi lain.

Tapi yang lain hanya deklarasi. Bayangkan Anda perlu menggunakannya int two(int bar); berfungsi di dalam metode utama. Tapi itu didefinisikan di bawah ini main() fungsi, sehingga deklarasi fungsi di dalam fungsi membuat Anda menggunakan fungsi tersebut dengan deklarasi.

Hal yang sama berlaku untuk yang ketiga. Deklarasi kelas di dalam fungsi memungkinkan Anda untuk menggunakan kelas di dalam fungsi tanpa memberikan header atau referensi yang tepat.

int main()
{
    // This is legal, but why would I want this?
    int two(int bar);

    //Call two
    int x = two(7);

    class three {
        int m_iBar;
        public:
            three(int bar):m_iBar(13 + bar) {}
            operator int() {return m_iBar;}
    };

    //Use class
    three *threeObj = new three();

    return 0;
}

5
2018-04-30 12:26



Fitur bahasa ini diwarisi dari C, di mana ia melayani beberapa tujuan di hari-hari awal C (deklarasi fungsi deklarasi mungkin?). Saya tidak tahu apakah fitur ini banyak digunakan oleh programmer C modern dan saya sangat meragukannya.

Jadi, untuk meringkas jawabannya:

tidak ada tujuan untuk fitur ini modern C ++ (yang saya tahu, setidaknya), itu di sini karena C + + - ke-C kompatibilitas (saya kira :)).


Terima kasih atas komentar di bawah ini:

Prototipe fungsi dicakupkan ke fungsi yang dideklarasikan, sehingga seseorang dapat memiliki namespace global yang lebih rapi - dengan merujuk ke fungsi eksternal / simbol tanpa #include.


4
2018-04-30 12:13



Sebenarnya, ada satu kasus penggunaan yang mungkin berguna. Jika Anda ingin memastikan bahwa fungsi tertentu dipanggil (dan kode Anda dikompilasi), tidak peduli apa pun yang dikandung kode sekitarnya, Anda dapat membuka blok Anda sendiri dan menyatakan prototipe fungsi di dalamnya. (Inspirasi berasal dari Johannes Schaub, https://stackoverflow.com/a/929902/3150802, melalui TeKa, https://stackoverflow.com/a/8821992/3150802).

Ini mungkin berguna jika Anda harus menyertakan header yang tidak Anda kontrol, atau jika Anda memiliki makro multi-baris yang dapat digunakan dalam kode yang tidak dikenal.

Kuncinya adalah bahwa deklarasi lokal menggantikan deklarasi sebelumnya di blok terlampir terdalam. Sementara itu dapat memperkenalkan bug halus (dan, saya pikir, dilarang di C #), itu dapat digunakan secara sadar. Mempertimbangkan:

// somebody's header
void f();

// your code
{   int i;
    int f(); // your different f()!
    i = f();
    // ...
}

Menautkan mungkin menarik karena kemungkinan adalah header milik perpustakaan, tapi saya kira Anda dapat menyesuaikan argumen linker sehingga f() diselesaikan ke fungsi Anda pada saat perpustakaan dipertimbangkan. Atau Anda mengatakannya untuk mengabaikan simbol duplikat. Atau Anda tidak terhubung dengan perpustakaan.


4
2018-04-30 12:40



Ini bukan jawaban atas pertanyaan OP, melainkan balasan untuk beberapa komentar.

Saya tidak setuju dengan poin-poin ini dalam komentar dan jawaban: 1 bahwa deklarasi bertingkat diduga tidak berbahaya, dan 2 bahwa definisi bersarang tidak berguna.

1 Jawaban utama untuk dugaan tidak berbahaya dari deklarasi fungsi bertingkat adalah terkenal Kebanyakan Vexing Parse. IMO penyebaran kebingungan yang disebabkan oleh itu cukup untuk menjamin aturan tambahan melarang deklarasi bertingkat.

2 The 1st counterexample ke dugaan tidak berguna dari definisi fungsi bertingkat adalah kebutuhan yang sering untuk melakukan operasi yang sama di beberapa tempat di dalam satu fungsi. Ada solusi yang jelas untuk ini:

private:
inline void bar(int abc)
{
    // Do the repeating operation
}

public: 
void foo()
{
    int a, b, c;
    bar(a);
    bar(b);
    bar(c);
}

Namun, solusi ini sering mencemari definisi kelas dengan banyak fungsi pribadi, yang masing-masing digunakan dalam satu pemanggil. Deklarasi fungsi bertingkat akan jauh lebih bersih.


3
2018-05-01 22:26