Чи можемо ми мати функції всередині функцій на C ++?


225

Я маю на увазі щось на кшталт:

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

  return 0;
}

1
Чому ти намагаєшся це зробити? Пояснення вашої мети може дозволити комусь сказати вам правильний шлях досягнення вашої мети.
Томас Оуенс

3
gcc підтримує вкладені функції як нестандартне розширення. Але краще не використовувати його, навіть якщо ви використовуєте gcc. А в режимі C ++ вона все одно недоступна.
Свен Марнах

27
@Thomas: Тому що було б добре зменшити область застосування? Функції у функціях - це звичайна функція в інших мовах.
Йохан Котлінський

64
Він говорить про вкладені функції. Так само, як і для наступних класів всередині класів, він хоче вкласти функцію всередині функції. Насправді, у мене були ситуації, коли я б і так робив, якби це було можливо. Існують мови (наприклад, F #), які це дозволяють, і я можу вам сказати, що він може зробити код набагато зрозумілішим, читабельнішим і доступнішим, не забруднюючи бібліотеку десятками допоміжних функцій, марних поза дуже специфічним контекстом. ;)
Мефан

16
@Thomas - вкладені функції можуть бути відмінним механізмом для злому складних функцій / алгоритмів без без заповнення поточної області з функціями, які НЕ загальне користування в межах обсягу вміщає. Паскаль і Ада мають чудову підтримку для них. Те саме з Scala та багатьма іншими старими / новими шанованими мовами. Як і будь-яка інша функція, їх також можна зловживати, але це функція розробника. ІМО, вони були набагато вигіднішими, ніж згубними.
luis.espinal

Відповіді:


271

Сучасний C ++ - Так, з лямбдами!

У поточних версіях c ++ (C ++ 11, C ++ 14 та C ++ 17) ви можете мати функції всередині функцій у вигляді лямбда:

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!"); 
    }
}

Лямбди також можуть змінювати локальні змінні за допомогою ** улаштування за посиланням *. Завдяки захопленню за посиланням лямбда має доступ до всіх локальних змінних, задекларованих в області лямбда. Він може змінювати та змінювати їх нормально.

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 та C ++ 03 - Не безпосередньо, але так зі статичними функціями всередині локальних класів

C ++ не підтримує це безпосередньо.

Це означає, що ви можете мати місцеві класи, і вони можуть мати функції (не ) staticабо static, так що ви можете отримати це до деякого розширення, хоч це і є трохи хитрості:

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

  X::a();

  return 0;
}

Однак я б поставив під сумнів практику. Усі знають (ну, тепер, коли ви це робите :)) C ++ не підтримує локальні функції, тому вони звикли їх не мати. Однак вони не звикли до цієї хитрості. Я витратив би досить часу на цей код, щоб переконатися, що він дійсно існує лише для дозволу локальних функцій. Не добре.


3
Main також займає два аргументи, якщо ви збираєтеся бути педантичними щодо типу повернення. :) (Або це необов'язково, але це не повернення в ці дні? Я не можу йти в ногу.)
Лео Девідсон

3
Це просто погано - це порушує кожну умову доброго, чистого коду. Я не можу придумати жодного екземпляра, де це гарна ідея.
Томас Оуенс

19
@Thomas Owens: Це добре, якщо вам потрібна функція зворотного дзвінка, і ви не хочете забруднювати її іншим простором імен.
Лев Девідсон

9
@Leo: Стандарт говорить, що для основної є дві допустимі форми: int main()іint main(int argc, char* argv[])
Джон Діблінг

8
Стандарт каже , int main()і int main(int argc, char* argv[])повинен підтримуватися та інші можуть бути підтримані , але всі вони мають зворотний Int.
JoeG

260

Для всіх цілей і цілей C ++ підтримує це через лямбдаси : 1

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

Тут fзнаходиться лямбда-об’єкт, який виконує роль локальної функції в main. Захоплення можна вказати, щоб функція мала доступ до локальних об'єктів.

За лаштунками f- це об'єкт функції (тобто об'єкт типу, який надає operator()). Тип об'єкта функції створюється компілятором на основі лямбда.


1, оскільки C ++ 11


5
Ах, це акуратно! Я не думав про це. Це набагато краще, ніж моя ідея, +1від мене.
sbi

1
@sbi: Я фактично використовував локальні структури, щоб імітувати це в минулому (так, я соромлюсь себе). Але корисність обмежується тим, що локальні структури не створюють закриття, тобто ви не можете отримати доступ до локальних змінних в них. Вам потрібно явно передавати та зберігати їх через конструктор.
Конрад Рудольф

1
@Konrad: Інша проблема з ними полягає в тому, що в C ++ 98 ви не повинні використовувати локальні типи як параметри шаблону. Я думаю, що C ++ 1x зняв це обмеження. (Або це був C ++ 03?)
sbi

3
@luis: Я повинен погодитися з Фредом. Ви надаєте значення лямбдам, яких у них просто немає (ні на C ++, ні на інших мовах, з якими я працював - які не включають Python та Ada для запису). Крім того, робити таке розрізнення просто не має сенсу в C ++, оскільки C ++ не має локальних функцій, періоду. У ньому є лише лямбда. Якщо ви хочете обмежити область функції, що нагадує функцію, функцією, ваш єдиний вибір - це лямбда або локальна структура, згадана в інших відповідях. Я б сказав, що останній досить заплутаний, щоб мати будь-який практичний інтерес.
Конрад Рудольф

2
@AustinWBryan Ні, лямбдаси на C ++ - це лише синтаксичний цукор для функторів і мають нульові накладні витрати. Десь на цьому веб-сайті є питання з більш детальною інформацією.
Конрад Рудольф

42

Про місцеві класи вже згадувалося, але ось спосіб дозволити їм виглядати ще більше як локальні функції, використовуючи перевантаження оператора () та анонімний клас:

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

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

Я не раджу використовувати це, це просто смішний трюк (можна, але імхо не повинен).


Оновлення 2014 року:

З часом підйом C ++ 11 тепер ви можете мати локальні функції, синтаксис яких трохи нагадує JavaScript:

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

1
Повинно бути operator () (unsigned int val), у вас відсутній набір дужок.
Джо D

1
Власне, це цілком розумна річ, якщо вам потрібно передати цей функтор до функції stl або алгоритму, наприклад std::sort(), або std::for_each().
Діма

1
@Dima: На жаль, у C ++ 03 локально визначені типи не можуть використовуватися як аргументи шаблону. C ++ 0x виправляє це, але також надає набагато приємніші рішення лямбда, так що ви все одно цього не робите.
Ben Voigt

На жаль, ви праві. Моє ліжко. Але все-таки це не просто кумедна хитрість. Було б корисно, якби це дозволили. :)
Діма

3
Рекурсія підтримується. Однак ви не можете використовувати autoдля оголошення змінної. Stroustrup наводить приклад: function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };для обернення рядка заданих покажчиків початку та кінця.
однойменний

17

Немає.

Що ти намагаєшся зробити?

вирішення:

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

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

}

2
Зауважимо, що підхід до інстанціалізації класів має розподіл пам'яті, а тому переважає статичний підхід.
ManuelSchneid3r

14

Починаючи з C ++ 11, ви можете використовувати правильні лямбдаши . Інші відповіді див. Для отримання більш детальної інформації.


Стара відповідь: Ви можете, начебто, але вам доведеться обдурити і використовувати манекен клас:

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

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

Не впевнені, що можете, хіба що створивши натомість об’єкт (який додає стільки ж шуму, IMO). Якщо є якась розумна річ, яку ви можете зробити з просторами імен, але я не можу про це думати, і, мабуть, не годиться зловживати мовою більше, ніж ми вже є. :)
Лев Девідсон

Позбавлення пустушки :: є в одній з інших відповідей.
Себастьян Мах

8

Як уже згадували інші, ви можете використовувати вкладені функції, використовуючи розширення мови gnu в gcc. Якщо ви (або ваш проект) дотримуєтесь ланцюжка інструментів gcc, ваш код буде здебільшого переносним для різних архітектур, на які спрямований компілятор gcc.

Однак якщо є можлива вимога, що вам може знадобитися компілювати код з іншою ланцюжком інструментів, я б не тримався осторонь таких розширень.


Я б також з обережністю ступав під час використання вкладених функцій. Вони є прекрасним рішенням для управління структурою складних, але згуртованих блоків коду (фрагменти яких не призначені для зовнішнього / загального використання.) Вони також дуже корисні для контролю забруднення простору імен (дуже реальна проблема з природно складними / тривалі заняття з багатослівних мов.)

Але як і все, вони можуть бути відкритими для зловживань.

Сумно, що C / C ++ не підтримує такі функції як стандарт. Більшість варіантів Pascal і Ada (майже всі мови на основі Algol). Те саме з JavaScript. Те саме з сучасними мовами, як Scala. Те саме з поважними мовами, такими як Erlang, Lisp або Python.

І так само, як і для C / C ++, на жаль, Java (з якою я заробляю більшу частину життя).

Тут я згадую Java, бо бачу кілька плакатів, які пропонують використання класів та методів класу як альтернативи вкладеним функціям. І це також типовий спосіб вирішення Java.

Коротка відповідь: Ні.

Це, як правило, вводить штучну, непотрібну складність в ієрархію класів. При рівних обставинах ідеальною є наявність ієрархії класів (та її охоплюючих просторів імен та областей), що представляє фактичний домен якомога простіше.

Вкладені функції допомагають розібратися з "приватною", всередині функціональної складності. Не маючи цих можливостей, слід намагатися уникати поширення цієї "приватної" складності і перетворюватися на свою класову модель.

У програмному забезпеченні (і в будь-якій інженерній дисципліні) моделювання - справа компромісів. Таким чином, у реальному житті будуть виправдані винятки з цих правил (а точніше - вказівок). Однак, будьте обережні.


8

Ви не можете мати локальні функції в C ++. Однак C ++ 11 має лямбда . Лямбди - це в основному змінні, які працюють як функції.

Лямбда має тип std::function( насправді це не зовсім вірно , але в більшості випадків ви можете припустити, що це так). Щоб використовувати цей тип, вам потрібно #include <functional>. std::functionє шаблоном, беручи за аргумент шаблону тип повернення та типи аргументів із синтаксисом std::function<ReturnType(ArgumentTypes). Наприклад, std::function<int(std::string, float)>це лямбда, що повертається intі приймає два аргументи, один std::stringі один float. Найпоширеніший з них - це те std::function<void()>, що нічого не повертає і не приймає аргументів.

Після оголошення лямбда він називається так само, як і звичайна функція, використовуючи синтаксис lambda(arguments).

Щоб визначити лямбда, використовуйте синтаксис [captures](arguments){code}(є й інші способи цього зробити, але я їх тут не згадую). arguments- це які аргументи бере лямбда, і codeце код, який слід запускати, коли лямбда викликається. Зазвичай ви ставите [=]або [&]як захоплюєте. [=]означає, що ви захоплюєте всі змінні в тій області, в якій значення визначається значенням, а значить, вони зберігатимуть значення, яке вони мали при оголошенні лямбда. [&]означає, що ви захоплюєте всі змінні в області за посиланням, це означає, що вони завжди матимуть своє поточне значення, але якщо їх стерти з пам'яті, програма вийде з ладу. Ось кілька прикладів:

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

Ви також можете захоплювати конкретні змінні, вказуючи їх імена. Щойно вказавши їх ім'я, вони захоплять їх за значенням, вказавши їх ім'я разом із значком " &попередній", зафіксувавши їх за посиланням. Наприклад, [=, &foo]буде fooохоплено всі змінні за значенням, за винятком того, що будуть захоплені посиланням, і [&, foo]буде охоплювати всі змінні за посиланням, за винятком того, fooщо будуть захоплені за значенням. Ви також можете захоплювати лише конкретні змінні, наприклад [&foo], фіксуватимете fooза посиланням і не буде охоплювати жодних інших змінних. Також ви можете взагалі не захоплювати змінних, використовуючи []. Якщо ви спробуєте використати змінну в лямбда, яку ви не захопили, вона не складеться. Ось приклад:

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

Ви не можете змінити значення змінної, яка була захоплена значенням всередині лямбда (змінні, захоплені значенням, мають constтип всередині лямбда). Для цього вам потрібно захопити змінну за посиланням. Ось іспит:

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

Крім того, виклик неініціалізованих лямбдаз - це невизначена поведінка і зазвичай призведе до збою програми. Наприклад, ніколи цього не роби:

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

Приклади

Ось код того, що ви хотіли зробити у своєму запитанні за допомогою лямбда:

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

Ось більш досконалий приклад лямбда:

#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

Ні, це не дозволено. Ні C, ні C ++ не підтримують цю функцію за замовчуванням, проте TonyK зазначає (у коментарях), що до компілятора GNU C є розширення, які дозволяють цю поведінку в C.


2
Він підтримується компілятором GNU C, як спеціальне розширення. Але тільки для C, а не C ++.
TonyK

Ага. У мого компілятора C у мене немає спеціальних розширень. Це добре знати, хоча. Я додам цей титк до своєї відповіді.
Томас Оуенс

Я використовував розширення gcc для підтримки вкладених функцій (в С, однак, не C ++). Вкладені функції - це чудова річ (як у Паскаля та Ада) для управління складними, але згуртованими структурами, які не призначені для загального використання. Поки хто використовує ланцюжок інструментів gcc, він, як правило, переносить всі цільові архітектури. Але якщо є необхідність компілювати отриманий код за допомогою компілятора non-gcc, тоді краще уникати таких розширень і дотримуватися якомога ближче до мантри ansi / posix.
luis.espinal

7

Всі ці трюки просто виглядають (більш-менш) як локальні функції, але вони не працюють так. У локальній функції ви можете використовувати локальні змінні її суперфункцій. Це свого роду напівглобали. Не з цих хитрощів можна це зробити. Найближчим є лямбда-трюк від c ++ 0x, але його закриття пов'язане у часі визначення, а не в часі використання.


Зараз я думаю, що це найкраща відповідь. Хоча можна оголосити функцію у межах функції (якою я весь час користуюся), вона не є локальною функцією, як визначено у багатьох інших мовах. Все ще добре знати можливість.
Алексіс Вілке

6

Ви не можете визначити вільну функцію всередині іншої в C ++.


1
Не з ansi / posix, але ви можете з розширеннями gnu.
luis.espinal

4

Дозвольте мені розмістити тут рішення для C ++ 03, яке вважаю найчистішим можливим. *

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

(*) у світі С ++ використання макросів ніколи не вважається чистим.


Алексісе, ти маєш рацію сказати, що це не зовсім чисто. Це ще близько до чистоти, оскільки добре виражає те, що мав намір програміст, без побічних ефектів. Я вважаю, що мистецтво програмування - це письмово виразне по-людськи, яке читається як роман.
Барні

2

Але ми можемо оголосити функцію всередині main ():

int main()
{
    void a();
}

Хоча синтаксис є правильним, іноді це може призвести до "Найпочутливішого розбору":

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

=> програму немає.

(Тільки попередження Clang після компіляції).

Знову найприємніший розбір C ++

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.