C ++ içindeki fonksiyonların içinde fonksiyonlara sahip olabilir miyiz?


226

Gibi bir şey demek istiyorum:

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

  return 0;
}

1
Bunu neden yapmaya çalışıyorsun? Amacınızı açıklamak, birisinin size hedefinize ulaşmanız için doğru yolu söylemesine izin verebilir.
Thomas Owens

3
gcc, standart olmayan bir uzantı olarak iç içe geçmiş işlevleri destekler . Ama gcc kullanıyor olsanız bile kullanmayın. Ve C ++ modunda, yine de kullanılamaz.
Sven Marnach

27
@Thomas: Çünkü a'nın kapsamını azaltmak iyi olur? İşlevlerdeki işlevler diğer dillerde olağan bir özelliktir.
Johan Kotlinski

64
Yuvalanmış işlevlerden bahsediyor. Sınıflar içindeki bir sonraki sınıflara benzer şekilde, bir fonksiyonu bir fonksiyonun içine yerleştirmek ister. Aslında, mümkünse ben de bunu yapabileceğim durumlar yaşadım. Buna izin veren diller (örn. F #) vardır ve size, çok özel bir bağlamın dışında yararsız düzinelerce yardımcı işlevi olan bir kütüphaneyi kirletmeden kodu çok daha net, okunabilir ve sürdürülebilir hale getirebileceğini söyleyebilirim. ;)
Mephane

16
@Thomas - iç içe fonksiyonlar kompleks fonksiyonlar / algoritmaları kırılması için mükemmel bir mekanizma olabilir olmayan işlevler mevcut kapsamını doldurmadan olmayan parça kapsamında genel kullanım. Pascal ve Ada, (IMO) onlara güzel destek veriyorlar. Scala ve diğer birçok eski / yeni saygın dil ile aynı. Diğer tüm özellikler gibi, bunlar da istismar edilebilir, ancak bu geliştiricinin bir işlevidir. IMO, bu kadar zararlı oldu.
luis.espinal

Yanıtlar:


272

Modern C ++ - Evet lambdas ile!

Geçerli c ++ sürümlerinde (C ++ 11, C ++ 14 ve C ++ 17), işlevlerin içinde lambda biçiminde işlevler olabilir:

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 ayrıca yerel referansları ** referansla yakalama * ile değiştirebilir. Referansla yakalama ile lambda, lambda kapsamında açıklanan tüm yerel değişkenlere erişebilir. Bunları normal olarak değiştirebilir ve değiştirebilir.

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 ve C ++ 03 - Doğrudan değil, ancak yerel sınıflardaki statik işlevlerle evet

C ++ bunu doğrudan desteklemez.

Bununla birlikte, yerel sınıflara sahip olabilirsiniz ve işlevlere sahip olabilirler ( staticya da değil static), bu yüzden biraz çamur olsa da, bunu biraz uzatabilirsiniz:

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

  X::a();

  return 0;
}

Ancak praksiyi sorgularım. Herkes biliyor (iyi, şimdi zaten, zaten :)) C ++ yerel işlevleri desteklemiyor, bu yüzden bunlara sahip değiller. Ancak bunlar bu çamura alışık değildir. Gerçekten sadece yerel işlevlere izin vermek için bu kod üzerinde biraz zaman harcamak istiyorum. İyi değil.


3
Main, dönüş türü hakkında bilgiçlik yapacaksanız iki argüman da alır. :) (Yoksa bu isteğe bağlı ama bu günlerde geri dönüş değil mi? Devam edemiyorum.)
Leo Davidson

3
Bu sadece kötü - iyi, temiz kodun her kuralını ihlal ediyor. Bunun iyi bir fikir olduğu tek bir örneği düşünemiyorum.
Thomas Owens

19
@Thomas Owens: Geri arama işlevine ihtiyacınız varsa ve onunla başka bir ad alanını kirletmek istemiyorsanız iyi olur.
Leo Davidson

9
@Leo: Standart, ana için izin verilen iki form olduğunu söylüyor: int main()veint main(int argc, char* argv[])
John Dibling

8
Standart diyor int main()ve int main(int argc, char* argv[])desteklenmeli ve diğerleri desteklenebilir ancak hepsinin dönüş int var.
JoeG

261

Tüm niyet ve amaçlar için, C ++ lambdas aracılığıyla bunu destekler : 1

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

Burada, fyerel bir işlev olarak hareket eden bir lambda nesnesidir main. Yakalama, işlevin yerel nesnelere erişmesine izin vermek için belirtilebilir.

Sahne arkasında fbir işlev nesnesi (yani an sağlayan türden bir nesne operator()). Fonksiyon nesnesi türü lambda tabanlı derleyici tarafından oluşturulur.


1 C ++ 11'den beri


5
Ah, bu temiz! Ben düşünmedim. Bu benim fikrimden çok daha iyi, +1benden.
sbi

1
sbi: Bunu geçmişte simüle etmek için yerel yapıları kullandım (evet, kendimden utanıyorum). Ancak kullanışlılık, yerel yapıların bir kapanış oluşturmadığı, yani bunlarda yerel değişkenlere erişemediğiniz gerçeğiyle sınırlıdır. Bunları bir kurucu aracılığıyla açıkça geçirmeniz ve depolamanız gerekir.
Konrad Rudolph

1
@Konrad: Onlarla ilgili başka bir sorun, C ++ 98'de şablon tür olarak yerel türleri kullanmamanızdır. Bence C ++ 1x bu kısıtlamayı kaldırdı. (Yoksa bu C ++ 03
müydü

3
@luis: Fred ile aynı fikirdeyim. Onların sadece (- ne C ++ ne de birlikte çalıştığım diğer dillerde yok lambdas anlam iliştiriyorsanız yok kayıt için Python ve Ada, ekleyin). Ayrıca, C ++ 'da yerel işlevlere sahip olmadığı için, bu ayrımı C ++' da anlamlı değildir. Sadece lambdalar var. İşlev benzeri bir şeyin kapsamını bir işlevle sınırlamak istiyorsanız, tek seçenekleriniz lambdalar veya diğer yanıtlarda belirtilen yerel yapıdır. İkincisinin herhangi bir pratik ilgi için çok kıvrık olduğunu söyleyebilirim.
Konrad Rudolph

2
@AustinWBryan Hayır, C ++ 'daki lambdalar functors için sadece sözdizimsel şekerdir ve ek yükü sıfırdır. Bu web sitesinde bir yerde daha ayrıntılı bir soru var.
Konrad Rudolph

43

Yerel sınıflardan daha önce bahsedilmiş, ancak işleci () aşırı yükü ve anonim bir sınıf kullanarak yerel işlevler olarak daha fazla görünmelerine izin vermenin bir yolu:

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

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

Bunu kullanmanızı tavsiye etmiyorum, sadece komik bir numara (yapabilir, ama imho olmamalı).


2014 Güncellemesi:

C ++ 11'in yükselişiyle birlikte, söz dizimi JavaScript'i biraz hatırlatan yerel işlevlere sahip olabilirsiniz:

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

1
Olmalı operator () (unsigned int val), eksik bir parantez kümesi.
Joe D

1
Aslında bu bir stl işlevi veya algoritma gibi bu functor geçmek gerekirse yapmaya son derece mantıklı bir şeydir std::sort()ya std::for_each().
Dima

1
@Dima: Maalesef, C ++ 03'te yerel olarak tanımlanmış türler şablon argümanları olarak kullanılamaz. C ++ 0x bunu düzeltir, ama aynı zamanda lambdasların çok daha güzel çözümlerini sağlar, bu yüzden yine de bunu yapmazsınız.
Ben Voigt

Hata! Haklısın. Benim hatam. Ama yine de, bu sadece komik bir numara değil. İzin verilirse faydalı bir şey olurdu. :)
Dima

3
Özyineleme desteklenir. Ancak, autodeğişkeni bildirmek için kullanamazsınız . Stroustrup, örneğin başlangıç function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };ve bitiş işaretçileri verilen bir dizgeyi tersine çevirmek için örnek verir .
İsimsiz

17

Hayır.

Ne yapmaya çalışıyorsun?

geçici çözüm:

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

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

}

2
Sınıf örnekleme yaklaşımının bir bellek ayırma ile geldiğini ve bu nedenle statik yaklaşımın egemen olduğunu unutmayın.
ManuelSchneid3r

14

C ++ 11 ile başlayarak uygun lambdaları kullanabilirsiniz . Daha fazla ayrıntı için diğer cevaplara bakın.


Eski cevap: Yapabilirsin, ama kukla bir sınıf almalısın ve kullanmalısın:

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

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

Bunun yerine bir nesne oluşturmak dışında bunu yapabileceğinizden emin değilsiniz (bu da çok fazla gürültü ekler, IMO). İsim alanları ile yapabileceğiniz akıllıca bir şey olmadığı sürece, ama düşünemiyorum ve muhtemelen dili zaten olduğumuzdan daha fazla kötüye kullanmak iyi bir fikir değil. :)
Leo Davidson

Kukladan kurtulmak :: diğer cevaplardan birindedir.
Sebastian Mach

8

Diğerlerinin de belirttiği gibi, gcc'de gnu dil uzantılarını kullanarak iç içe geçmiş işlevleri kullanabilirsiniz. Siz (veya projeniz) gcc araç zincirine yapışırsanız, kodunuz çoğunlukla gcc derleyicisinin hedeflediği farklı mimariler arasında taşınabilir olacaktır.

Ancak, kodu farklı bir araç zinciriyle derlemeniz gerekebilecek olası bir gereksinim varsa, bu tür uzantılardan uzak dururum.


Ayrıca, iç içe geçmiş işlevleri kullanırken de dikkatli davranırım. Karmaşık, ancak birbirine bağlı kod bloklarının (parçaları harici / genel kullanım için amaçlanmayan) yapısını yönetmek için güzel bir çözümdür. Ayrıca ad alanı kirliliğini kontrol etmede çok yararlıdırlar (doğal olarak karmaşık / ayrıntılı dillerde uzun sınıflar.)

Ama her şey gibi, onlar da istismara açık olabilirler.

C / C ++ 'nın standart gibi özellikleri desteklememesi üzücü. Çoğu paskal varyant ve Ada yapar (neredeyse tüm Algol tabanlı diller yapar). JavaScript ile aynı. Scala gibi modern dillerle aynı. Erlang, Lisp veya Python gibi saygıdeğer dillerle aynı.

Ve C / C ++ ile olduğu gibi maalesef Java (benim hayatımın çoğunu kazandığım) bunu yapmaz.

Burada Java'dan bahsediyorum çünkü sınıfların ve sınıf yöntemlerinin iç içe işlevlere alternatif olarak kullanılmasını öneren birkaç poster görüyorum. Bu da Java'daki tipik çözümdür.

Kısa cevap: Hayır.

Bunu yapmak, sınıf hiyerarşisine yapay, gereksiz karmaşıklık getirme eğilimindedir. Her şey eşit olduğunda, ideal, gerçek bir alanı mümkün olduğunca basit bir şekilde temsil eden bir sınıf hiyerarşisine (ve onu kapsayan ad alanlarına ve kapsamlarına) sahip olmaktır.

Yuvalanmış işlevler, işlev içinde karmaşıklığın "özel" olarak ele alınmasına yardımcı olur. Bu tesislerden yoksun olan kişi, bu "özel" karmaşıklığı sınıf modeline yaymaktan kaçınmaya çalışmalıdır.

Yazılımda (ve herhangi bir mühendislik disiplininde), modelleme bir değiş tokuş meselesidir. Böylece, gerçek hayatta, bu kuralların (veya daha doğrusu kılavuzların) haklı istisnaları olacaktır. Yine de dikkatli olun.


8

C ++ 'da yerel işlevleriniz olamaz. Bununla birlikte, C ++ 11'in lambdaları vardır . Lambdalar temelde fonksiyonlar gibi çalışan değişkenlerdir.

Bir lambda türü vardır std::function( aslında bu oldukça doğru değildir , ancak çoğu durumda öyle olduğunu varsayabilirsiniz). Bu türü kullanmak için kullanmanız gerekir #include <functional>. std::functionsözdizimi ile dönüş argümanı ve argüman türlerini şablon argümanı olarak alan bir şablondur std::function<ReturnType(ArgumentTypes). Örneğin std::function<int(std::string, float)>, bir döndüren intve bir std::stringve bir olmak üzere iki argüman alan bir lambdadır float. En yaygın olanı, std::function<void()>hiçbir şey döndürmez ve argüman almaz.

Bir lambda bildirildikten sonra, sözdizimi kullanılarak normal bir işlev gibi çağrılır lambda(arguments).

Bir lambda tanımlamak için sözdizimini kullanın [captures](arguments){code}(bunu yapmanın başka yolları vardır, ancak burada bahsetmeyeceğim). argumentslambda'nın aldığı argümanlar ve lambda codeçağrıldığında çalıştırılması gereken koddur. Genellikle koyar [=]ya [&]da yakalarsınız. [=]bu, değerin değerle tanımlandığı kapsamdaki tüm değişkenleri yakaladığınız anlamına gelir. Bu, lambda bildirildiğinde sahip oldukları değeri koruyacakları anlamına gelir. [&]kapsamdaki tüm değişkenleri referans olarak yakaladığınız anlamına gelir, yani her zaman geçerli değerlerine sahip olurlar, ancak bellekten silinirlerse program çökecektir. İşte bazı örnekler:

#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;
}

Belirli değişkenleri adlarını belirterek de yakalayabilirsiniz. Sadece isimlerini belirtmek onları değere göre yakalar, isimlerini daha &önce belirtmek onları referans olarak yakalar. Örneğin, referans tarafından yakalanacak [=, &foo]olanlar hariç tüm değişkenleri değere göre yakalar ve değer tarafından fooyakalanacak [&, foo]olanlar dışında tüm değişkenleri referans olarak yakalar foo. Örnek için de, sadece belirli değişkenler yakalayabilir [&foo]yakalayacaktır fooreferans olarak ve başka hiçbir değişkenleri ele geçirecek. Düğmesini kullanarak hiçbir değişkeni yakalayamazsınız []. Eğer yakalamadığınız bir lambda'da bir değişken kullanmaya çalışırsanız, derlenmez. İşte bir örnek:

#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;
}

Lambda içindeki değerle yakalanan bir değişkenin değerini değiştiremezsiniz (değerle yakalanan değişkenlerin constlambda içinde bir türü vardır). Bunu yapmak için değişkeni başvuru ile yakalamanız gerekir. İşte bir örnek:

#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;
}

Ayrıca, başlatılmamış lambdas çağırmak tanımsız bir davranıştır ve genellikle programın çökmesine neden olur. Örneğin, bunu asla yapmayın:

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

Örnekler

Lamdas kullanarak sorunuzda ne yapmak istediğinize ilişkin kod:

#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;
}

İşte lambda'nın daha gelişmiş bir örneği:

#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

Hayır, izin verilmiyor. Ne C ne de C ++ bu özelliği varsayılan olarak desteklemez, ancak TonyK (yorumlarda), C'de bu davranışı etkinleştiren GNU C derleyicisinin uzantıları olduğuna dikkat çeker.


2
GNU C derleyicisi tarafından özel bir uzantı olarak desteklenir. Ancak sadece C için, C ++ için değil.
TonyK

Ah. C derleyicimde özel bir uzantı yok. Yine de bunu bilmek güzel. Cevabıma bu titbit'i ekleyeceğim.
Thomas Owens

Ben iç içe işlevleri desteklemek için gcc uzantısını kullandım (C, ama, C ++ değil). Yuvalanmış işlevler, genel kullanım amaçlı olmayan karmaşık ancak birbirine bağlı yapıları yönetmek için şık bir şeydir (Pascal ve Ada'da olduğu gibi). Bir kişi gcc araç zincirini kullandığı sürece, çoğunlukla hedeflenen tüm mimariler için taşınabilir olduğundan emin olunur . Ancak, sonuç kodunu gcc olmayan bir derleyici ile derleme zorunluluğu varsa, bu tür uzantılardan kaçınmak ve ansi / posix mantraya olabildiğince yakın olmak en iyisidir.
luis.espinal

7

Tüm bu püf noktaları sadece (az ya da çok) yerel işlevler olarak görünüyor, ancak böyle çalışmıyorlar. Yerel bir işlevde, süper işlevlerinin yerel değişkenlerini kullanabilirsiniz. Bir çeşit yarı küresel. Bu hilelerin hiçbiri bunu yapamaz. En yakın olan c ++ 0x'den lambda hüneridir, ancak kapanış kullanım süresine değil, tanım süresine bağlıdır.


Şimdi bunun en iyi cevap olduğunu düşünüyorum. Bir işlev içindeki bir işlevi (her zaman kullandığım) bildirmek mümkün olsa da, diğer birçok dilde tanımlandığı gibi yerel bir işlev değildir. Olasılığı bilmek hala iyidir.
Alexis Wilke

6

C ++ 'da başka bir fonksiyonun içinde serbest fonksiyon tanımlayamazsınız.


1
Ansi / posix ile değil, gnu uzantıları ile yapabilirsiniz.
luis.espinal

4

Burada mümkün olan en temiz olduğunu düşündüğüm C ++ 03 için bir çözüm göndereyim. *

#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;
}

(*) C ++ dünyasında makro kullanan hiçbir zaman temiz kabul edilmez.


Alexis, bunun tamamen temiz olmadığını söylemekte haklısın. Hala temiz olmaya yakındır, çünkü programcının yan etkileri olmadan ne yapması gerektiğini iyi ifade eder. Programlama sanatının, bir roman gibi okunan, insan tarafından okunabilir şekilde etkileyici olduğunu düşünüyorum.
Barney

2

Ancak main () içindeki bir işlevi bildirebiliriz:

int main()
{
    void a();
}

Sözdizimi doğru olmasına rağmen, bazen "En sinir bozucu ayrıştırmaya" yol açabilir:

#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());
}

=> program çıkışı yok.

(Sadece derlemeden sonra Clang uyarısı).

C ++ 'ın en sinir bozucu ayrışması yine

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.