Bisakah kita memiliki fungsi di dalam fungsi dalam C ++?


225

Maksud saya sesuatu seperti:

int main() 
{
  void a() 
  {
      // code
  }
  a();

  return 0;
}

1
Mengapa Anda mencoba melakukan ini? Menjelaskan tujuan Anda mungkin memungkinkan seseorang memberi tahu Anda cara yang tepat untuk mencapai tujuan Anda.
Thomas Owens

3
gcc mendukung fungsi bersarang sebagai ekstensi non-standar. Tetapi lebih baik jangan menggunakannya bahkan jika Anda menggunakan gcc. Dan dalam mode C ++, toh itu tidak tersedia.
Sven Marnach

27
@ Thomas: Karena itu akan baik untuk mengurangi ruang lingkup? Fungsi dalam fungsi adalah fitur biasa dalam bahasa lain.
Johan Kotlinski

64
Dia berbicara tentang fungsi bersarang. Demikian pula untuk dapat kelas berikutnya di dalam kelas, ia ingin membuat fungsi di dalam fungsi. Sebenarnya, saya memiliki situasi di mana saya akan melakukannya juga, jika itu mungkin. Ada bahasa (mis. F #) yang memungkinkan ini, dan saya dapat memberitahu Anda bahwa itu dapat membuat kode lebih jelas, mudah dibaca, dan dipelihara tanpa mencemari perpustakaan dengan puluhan fungsi pembantu yang tidak berguna di luar konteks yang sangat spesifik. ;)
Mephane

16
@Thomas - fungsi bersarang dapat menjadi mekanisme yang sangat baik untuk memecahkan fungsi / algoritma yang kompleks tanpa tanpa mengisi ruang lingkup saat ini dengan fungsi-fungsi yang tidak umum digunakan dalam lingkup yang melampirkan. Pascal dan Ada memiliki (IMO) dukungan yang bagus untuk mereka. Sama dengan Scala dan banyak bahasa lama / baru lainnya yang dihormati. Seperti fitur lainnya, mereka juga dapat disalahgunakan, tetapi itu adalah fungsi pengembang. IMO, mereka jauh lebih bermanfaat yang merugikan.
luis.espinal

Jawaban:


271

Modern C ++ - Ya dengan lambdas!

Dalam versi c ++ (C ++ 11, C ++ 14, dan C ++ 17) saat ini, Anda dapat memiliki fungsi di dalam fungsi dalam bentuk lambda:

int main() {
    // This declares a lambda, which can be called just like a function
    auto print_message = [](std::string message) 
    { 
        std::cout << message << "\n"; 
    };

    // Prints "Hello!" 10 times
    for(int i = 0; i < 10; i++) {
        print_message("Hello!"); 
    }
}

Lambdas juga dapat memodifikasi variabel lokal melalui ** capture-by-reference *. Dengan menangkap-oleh-referensi, lambda memiliki akses ke semua variabel lokal yang dinyatakan dalam cakupan lambda. Itu dapat memodifikasi dan mengubahnya secara normal.

int main() {
    int i = 0;
    // Captures i by reference; increments it by one
    auto addOne = [&] () {
        i++; 
    };

    while(i < 10) {
        addOne(); //Add 1 to i
        std::cout << i << "\n";
    }
}

C ++ 98 dan C ++ 03 - Tidak secara langsung, tapi ya dengan fungsi statis di dalam kelas lokal

C ++ tidak mendukung itu secara langsung.

Yang mengatakan, Anda dapat memiliki kelas-kelas lokal, dan mereka dapat memiliki fungsi (bukan staticatau static), sehingga Anda dapat memperluasnya, meskipun itu sedikit membosankan:

int main() // it's int, dammit!
{
  struct X { // struct's as good as class
    static void a()
    {
    }
  };

  X::a();

  return 0;
}

Namun, saya akan mempertanyakan praksis. Semua orang tahu (yah, sekarang Anda tahu, :)C ++ tidak mendukung fungsi lokal, jadi mereka terbiasa tidak memilikinya. Namun, itu tidak digunakan untuk lumpur itu. Saya akan menghabiskan waktu cukup lama pada kode ini untuk memastikan itu benar-benar hanya ada untuk memungkinkan fungsi lokal. Tidak baik.


3
Main juga membutuhkan dua argumen jika Anda akan bertele-tele tentang tipe pengembalian. :) (Atau apakah itu opsional tetapi bukan kembalinya hari ini? Saya tidak bisa mengikutinya.)
Leo Davidson

3
Ini buruk - ia merusak setiap konvensi dari kode yang baik dan bersih. Saya tidak bisa memikirkan satu pun contoh di mana ini adalah ide yang bagus.
Thomas Owens

19
@ Thomas Owens: Ada baiknya jika Anda membutuhkan fungsi panggilan balik dan tidak ingin mencemari beberapa namespace lainnya dengannya.
Leo Davidson

9
@ Leo: Standar mengatakan ada dua formulir utama yang diizinkan: int main()danint main(int argc, char* argv[])
John Dibling

8
Standar mengatakan int main()dan int main(int argc, char* argv[])harus didukung dan yang lain mungkin didukung tetapi mereka semua memiliki int kembali.
JoeG

260

Untuk semua maksud dan tujuan, C ++ mendukung ini melalui lambdas : 1

int main() {
    auto f = []() { return 42; };
    std::cout << "f() = " << f() << std::endl;
}

Di sini, fadalah objek lambda yang bertindak sebagai fungsi lokal di main. Capture dapat ditentukan untuk memungkinkan fungsi mengakses objek lokal.

Di belakang layar, fadalah objek fungsi (yaitu objek dari jenis yang menyediakan sebuah operator()). Tipe objek fungsi dibuat oleh kompiler berdasarkan pada lambda.


1 sejak C ++ 11


5
Ah, itu rapi! Saya tidak memikirkannya. Ini jauh lebih baik daripada ide saya, +1dari saya.
sbi

1
@ Sbi: Saya sebenarnya menggunakan struct lokal untuk mensimulasikan ini di masa lalu (ya, saya malu pada diri saya sendiri). Tetapi kegunaan dibatasi oleh fakta bahwa struct lokal tidak membuat penutupan, yaitu Anda tidak dapat mengakses variabel lokal di dalamnya. Anda harus lulus dan menyimpannya secara eksplisit melalui konstruktor.
Konrad Rudolph

1
@ Konrad: Masalah lain dengan mereka adalah bahwa di C ++ 98 Anda tidak boleh menggunakan tipe lokal sebagai parameter template. Saya pikir C ++ 1x telah mengangkat batasan itu. (Atau apakah itu C ++ 03?)
sbi

3
@luis: Saya harus setuju dengan Fred. Anda melampirkan makna pada lambdas yang tidak mereka miliki (baik dalam C ++ maupun dalam bahasa lain yang telah saya kerjakan - yang tidak termasuk Python dan Ada, sebagai catatan). Lebih jauh, membuat perbedaan itu tidak berarti dalam C ++ karena C ++ tidak memiliki fungsi lokal, titik. Hanya memiliki lambda. Jika Anda ingin membatasi ruang lingkup hal yang mirip fungsi ke fungsi, satu-satunya pilihan Anda adalah lambdas atau struct lokal yang disebutkan dalam jawaban lain. Saya akan mengatakan bahwa yang terakhir agak terlalu rumit untuk kepentingan praktis.
Konrad Rudolph

2
@AustinWBryan Tidak, lambdas di C ++ hanyalah gula sintaksis untuk functors dan tidak memiliki overhead. Ada pertanyaan dengan detail lebih lanjut di suatu tempat di situs web ini.
Konrad Rudolph

42

Kelas-kelas lokal telah disebutkan, tetapi berikut ini adalah cara untuk menjadikannya lebih sebagai fungsi lokal, menggunakan operator () yang berlebihan dan kelas anonim:

int main() {
    struct {
        unsigned int operator() (unsigned int val) const {
            return val<=1 ? 1 : val*(*this)(val-1);
        }
    } fac;

    std::cout << fac(5) << '\n';
}

Saya tidak menyarankan untuk menggunakan ini, itu hanya trik lucu (bisa dilakukan, tetapi seharusnya tidak).


Pembaruan 2014:

Dengan munculnya C ++ 11 beberapa waktu lalu, Anda sekarang dapat memiliki fungsi lokal yang sintaksnya sedikit mengingatkan pada JavaScript:

auto fac = [] (unsigned int val) {
    return val*42;
};

1
Seharusnya operator () (unsigned int val), Anda kehilangan satu set kurung.
Joe D

1
Sebenarnya, ini adalah hal yang sangat masuk akal untuk dilakukan jika Anda perlu meneruskan functor ini ke fungsi atau algoritma stl, seperti std::sort(), atau std::for_each().
Dima

1
@Dima: Sayangnya, di C ++ 03, tipe yang didefinisikan secara lokal tidak dapat digunakan sebagai argumen templat. C ++ 0x memperbaiki ini, tetapi juga menyediakan solusi lambdas yang jauh lebih baik, sehingga Anda tetap tidak akan melakukannya.
Ben Voigt

Ups, Anda benar. Salahku. Tapi tetap saja, ini bukan sekadar trik lucu. Itu akan menjadi hal yang berguna jika diizinkan. :)
Dima

3
Rekursi didukung. Namun, Anda tidak dapat menggunakan autountuk mendeklarasikan variabel. Stroustrup memberikan contoh: function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };untuk membalikkan string yang diberikan awal dan akhir pointer.
Eponim

17

Tidak.

Apa yang sedang Anda coba lakukan?

solusi:

int main(void)
{
  struct foo
  {
    void operator()() { int a = 1; }
  };

  foo b;
  b(); // call the operator()

}

2
Perhatikan bahwa pendekatan instantiasi kelas hadir dengan alokasi memori dan karenanya didominasi oleh pendekatan statis.
ManuelSchneid3r

14

Dimulai dengan C ++ 11 Anda dapat menggunakan lambdas yang tepat . Lihat jawaban lain untuk lebih jelasnya.


Jawaban lama: Anda bisa, semacam, tetapi Anda harus menipu dan menggunakan kelas dummy:

void moo()
{
    class dummy
    {
    public:
         static void a() { printf("I'm in a!\n"); }
    };

    dummy::a();
    dummy::a();
}

Tidak yakin Anda bisa, kecuali dengan membuat objek sebagai gantinya (yang menambahkan banyak noise, IMO). Kecuali ada beberapa hal pintar yang bisa Anda lakukan dengan ruang nama, tapi saya tidak bisa memikirkannya dan mungkin bukan ide yang baik untuk menyalahgunakan bahasa itu lebih dari yang sudah ada. :)
Leo Davidson

The get-rid-of-dummy :: ada di salah satu jawaban lain.
Sebastian Mach

8

Seperti yang disebutkan orang lain, Anda dapat menggunakan fungsi bersarang dengan menggunakan ekstensi bahasa gnu di gcc. Jika Anda (atau proyek Anda) menempel pada rantai alat gcc, kode Anda sebagian besar portabel di berbagai arsitektur yang ditargetkan oleh kompiler gcc.

Namun, jika ada persyaratan yang mungkin Anda perlukan untuk mengkompilasi kode dengan toolchain yang berbeda, maka saya akan tinggal jauh dari ekstensi tersebut.


Saya juga melangkah dengan hati-hati saat menggunakan fungsi bersarang. Mereka adalah solusi yang indah untuk mengelola struktur blok kode yang rumit, namun kohesif (potongan-potongan yang tidak dimaksudkan untuk penggunaan eksternal / umum). Mereka juga sangat membantu dalam mengendalikan polusi namespace (perhatian yang sangat nyata dengan kompleks alami / kelas panjang dalam bahasa verbose.)

Tapi seperti apa pun, mereka bisa terbuka terhadap pelecehan.

Sangat menyedihkan bahwa C / C ++ tidak mendukung fitur-fitur seperti standar. Kebanyakan varian pascal dan Ada lakukan (hampir semua bahasa berbasis Algol melakukannya). Sama dengan JavaScript. Sama dengan bahasa modern seperti Scala. Sama dengan bahasa terhormat seperti Erlang, Lisp atau Python.

Dan seperti halnya dengan C / C ++, sayangnya, Java (dengan mana saya mendapatkan sebagian besar hidup saya) tidak.

Saya menyebutkan Java di sini karena saya melihat beberapa poster menyarankan penggunaan kelas dan metode kelas sebagai alternatif untuk fungsi bersarang. Dan itu juga solusi khas di Jawa.

Jawaban singkat: Tidak.

Melakukannya cenderung memperkenalkan kompleksitas buatan, yang tidak perlu pada hierarki kelas. Dengan semua hal yang sama, yang ideal adalah memiliki hierarki kelas (dan ruang lingkup dan cakupannya) yang mewakili domain aktual sesederhana mungkin.

Fungsi bersarang membantu menangani kompleksitas "pribadi" dalam fungsi. Karena kekurangan fasilitas-fasilitas itu, orang harus berusaha menghindari penyebaran kompleksitas "pribadi" itu ke dalam model kelasnya.

Dalam perangkat lunak (dan dalam disiplin ilmu teknik apa pun), pemodelan merupakan masalah pertukaran. Dengan demikian, dalam kehidupan nyata, akan ada pengecualian yang dibenarkan untuk aturan-aturan tersebut (atau lebih tepatnya pedoman). Lanjutkan dengan hati-hati.


8

Anda tidak dapat memiliki fungsi lokal di C ++. Namun, C ++ 11 memiliki lambdas . Lambdas pada dasarnya adalah variabel yang berfungsi seperti fungsi.

Lambda memiliki tipe std::function( sebenarnya itu tidak sepenuhnya benar , tetapi dalam kebanyakan kasus Anda dapat mengira itu adalah). Untuk menggunakan jenis ini, Anda harus #include <functional>. std::functionadalah templat, dengan argumen templat, tipe pengembalian dan tipe argumen, dengan sintaks std::function<ReturnType(ArgumentTypes). Misalnya, std::function<int(std::string, float)>adalah lambda mengembalikan intdan mengambil dua argumen, satu std::stringdan satu float. Yang paling umum adalahstd::function<void()> , yang tidak menghasilkan apa-apa dan tidak mengambil argumen.

Setelah lambda dideklarasikan, ia dipanggil seperti fungsi normal, menggunakan sintaks lambda(arguments) .

Untuk mendefinisikan lambda, gunakan sintaks [captures](arguments){code}(ada cara lain untuk melakukannya, tapi saya tidak akan menyebutkannya di sini). argumentsadalah argumen yang diambil oleh lambda, dan codemerupakan kode yang harus dijalankan ketika lambda dipanggil. Biasanya Anda menempatkan [=]atau [&]sebagai tangkapan. [=]berarti bahwa Anda menangkap semua variabel dalam lingkup di mana nilai ditentukan oleh nilai, yang berarti bahwa mereka akan menyimpan nilai yang mereka miliki ketika lambda dideklarasikan. [&]berarti bahwa Anda menangkap semua variabel dalam lingkup dengan referensi, yang berarti bahwa mereka akan selalu memiliki nilai saat ini, tetapi jika mereka dihapus dari memori, program akan macet. Berikut ini beberapa contohnya:

#include <functional>
#include <iostream>

int main(){
    int x = 1;

    std::function<void()> lambda1 = [=](){
        std::cout << x << std::endl;
    };
    std::function<void()> lambda2 = [&](){
        std::cout << x << std::endl;
    };

    x = 2;
    lambda1();    //Prints 1 since that was the value of x when it was captured and x was captured by value with [=]
    lambda2();    //Prints 2 since that's the current value of x and x was captured by value with [&]

    std::function<void()> lambda3 = [](){}, lambda4 = [](){};    //I prefer to initialize these since calling an uninitialized lambda is undefined behavior.
                                                                 //[](){} is the empty lambda.

    {
        int y = 3;    //y will be deleted from the memory at the end of this scope
        lambda3 = [=](){
            std::cout << y << endl;
        };
        lambda4 = [&](){
            std::cout << y << endl;
        };
    }

    lambda3();    //Prints 3, since that's the value y had when it was captured

    lambda4();    //Causes the program to crash, since y was captured by reference and y doesn't exist anymore.
                  //This is a bit like if you had a pointer to y which now points nowhere because y has been deleted from the memory.
                  //This is why you should be careful when capturing by reference.

    return 0;
}

Anda juga dapat menangkap variabel tertentu dengan menentukan nama mereka. Hanya menentukan nama mereka akan menangkap mereka dengan nilai, menentukan nama mereka dengan &sebelum akan menangkap mereka dengan referensi. Misalnya, [=, &foo]akan menangkap semua variabel dengan nilai kecuali fooyang akan ditangkap oleh referensi, dan [&, foo]akan menangkap semua variabel dengan referensi kecuali fooyang akan ditangkap oleh nilai. Anda juga dapat menangkap hanya variabel tertentu, misalnya [&foo]akan menangkap foodengan referensi dan tidak akan menangkap variabel lain. Anda juga dapat menangkap tidak ada variabel sama sekali dengan menggunakan []. Jika Anda mencoba menggunakan variabel dalam lambda yang tidak Anda tangkap, itu tidak akan dikompilasi. Berikut ini sebuah contoh:

#include <functional>

int main(){
    int x = 4, y = 5;

    std::function<void(int)> myLambda = [y](int z){
        int xSquare = x * x;    //Compiler error because x wasn't captured
        int ySquare = y * y;    //OK because y was captured
        int zSquare = z * z;    //OK because z is an argument of the lambda
    };

    return 0;
}

Anda tidak dapat mengubah nilai variabel yang ditangkap oleh nilai di dalam lambda (variabel yang ditangkap oleh nilai memiliki constjenis di dalam lambda). Untuk melakukannya, Anda perlu menangkap variabel dengan referensi. Inilah contohnya:

#include <functional>

int main(){
    int x = 3, y = 5;
    std::function<void()> myLambda = [x, &y](){
        x = 2;    //Compiler error because x is captured by value and so it's of type const int inside the lambda
        y = 2;    //OK because y is captured by reference
    };
    x = 2;    //This is of course OK because we're not inside the lambda
    return 0;
}

Juga, memanggil lambdas tidak diinisialisasi adalah perilaku yang tidak terdefinisi dan biasanya akan menyebabkan program macet. Misalnya, jangan pernah melakukan ini:

std::function<void()> lambda;
lambda();    //Undefined behavior because lambda is uninitialized

Contohnya

Berikut adalah kode untuk apa yang ingin Anda lakukan dalam pertanyaan Anda menggunakan lambdas:

#include <functional>    //Don't forget this, otherwise you won't be able to use the std::function type

int main(){
    std::function<void()> a = [](){
        // code
    }
    a();
    return 0;
}

Berikut adalah contoh lambda yang lebih maju:

#include <functional>    //For std::function
#include <iostream>      //For std::cout

int main(){
    int x = 4;
    std::function<float(int)> divideByX = [x](int y){
        return (float)y / (float)x;    //x is a captured variable, y is an argument
    }
    std::cout << divideByX(3) << std::endl;    //Prints 0.75
    return 0;
}

7

Tidak, itu tidak diizinkan. Baik C maupun C ++ tidak mendukung fitur ini secara default, namun TonyK menunjukkan (dalam komentar) bahwa ada ekstensi ke kompiler GNU C yang memungkinkan perilaku ini dalam C.


2
Ini didukung oleh kompiler GNU C, sebagai ekstensi khusus. Tetapi hanya untuk C, bukan C ++.
TonyK

Ah. Saya tidak memiliki ekstensi khusus di kompiler C saya. Itu bagus untuk diketahui. Saya akan menambahkan titbit itu ke jawaban saya.
Thomas Owens

Saya telah menggunakan ekstensi gcc untuk mendukung fungsi bersarang (dalam C, meskipun, bukan C ++). Fungsi bersarang adalah hal yang bagus (seperti dalam Pascal dan Ada) untuk mengelola struktur yang kohesif namun kompleks yang tidak dimaksudkan untuk penggunaan umum. Selama seseorang menggunakan gcc toolchain, dipastikan sebagian besar portabel untuk semua arsitektur yang ditargetkan. Tetapi jika ada perubahan karena harus mengkompilasi kode yang dihasilkan dengan kompiler non-gcc, maka, yang terbaik adalah menghindari ekstensi tersebut dan tetap sedekat mungkin dengan mantra ansi / posix.
luis.espinal

7

Semua trik ini hanya terlihat (kurang lebih) sebagai fungsi lokal, tetapi mereka tidak berfungsi seperti itu. Dalam fungsi lokal Anda dapat menggunakan variabel lokal dari fungsi super itu. Ini semacam semi-global. Tak satu pun dari trik ini bisa melakukannya. Yang paling dekat adalah trik lambda dari c ++ 0x, tetapi penutupannya terikat pada waktu definisi, bukan waktu penggunaan.


Sekarang saya pikir ini adalah jawaban terbaik. Meskipun dimungkinkan untuk mendeklarasikan fungsi dalam suatu fungsi (yang saya gunakan setiap saat,) itu bukan fungsi lokal seperti yang didefinisikan dalam banyak bahasa lain. Masih bagus untuk mengetahui kemungkinannya.
Alexis Wilke

6

Anda tidak dapat mendefinisikan fungsi bebas di dalam yang lain di C ++.


1
Tidak dengan ansi / posix, tetapi Anda bisa dengan ekstensi gnu.
luis.espinal

4

Biarkan saya memposting solusi di sini untuk C ++ 03 yang saya anggap sebagai yang terbersih mungkin. *

#define DECLARE_LAMBDA(NAME, RETURN_TYPE, FUNCTION) \
    struct { RETURN_TYPE operator () FUNCTION } NAME;

...

int main(){
  DECLARE_LAMBDA(demoLambda, void, (){ cout<<"I'm a lambda!"<<endl; });
  demoLambda();

  DECLARE_LAMBDA(plus, int, (int i, int j){
    return i+j;
  });
  cout << "plus(1,2)=" << plus(1,2) << endl;
  return 0;
}

(*) di dunia C ++ menggunakan makro tidak pernah dianggap bersih.


Alexis, Anda benar mengatakan bahwa itu tidak sepenuhnya bersih. Masih hampir bersih karena juga mengekspresikan apa yang dimaksudkan oleh programmer, tanpa efek samping. Saya menganggap seni pemrograman adalah menulis ekspresif yang dapat dibaca manusia yang berbunyi seperti sebuah novel.
Barney

2

Tapi kita bisa mendeklarasikan fungsi di dalam main ():

int main()
{
    void a();
}

Meskipun sintaksnya benar, kadang-kadang dapat menyebabkan "Parsing paling menjengkelkan":

#include <iostream>


struct U
{
    U() : val(0) {}
    U(int val) : val(val) {}

    int val;
};

struct V
{
    V(U a, U b)
    {
        std::cout << "V(" << a.val << ", " << b.val << ");\n";
    }
    ~V()
    {
        std::cout << "~V();\n";
    }
};

int main()
{
    int five = 5;
    V v(U(five), U());
}

=> tidak ada output program.

(Hanya peringatan dentang setelah kompilasi).

C ++ yang paling menjengkelkan mengurai lagi

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.