Можем ли мы иметь функции внутри функций в C ++?


225

Я имею в виду что-то вроде:

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

  return 0;
}

1
Почему вы пытаетесь это сделать? Объяснение вашей цели может позволить кому-то сказать вам правильный путь для достижения вашей цели.
Томас Оуэнс

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

27
@ Томас: Потому что было бы хорошо, чтобы уменьшить область? Функции в функциях - обычная функция в других языках.
Йохан Котлински

64
Он говорит о вложенных функциях. Подобно тому, как он может перейти к следующим классам внутри классов, он хочет вложить функцию внутри функции. На самом деле, у меня были ситуации, когда я бы тоже так сделал, если бы это было возможно. Существуют языки (например, F #), которые позволяют это, и я могу вам сказать, что он может сделать код намного более понятным, читаемым и обслуживаемым, не загрязняя библиотеку десятками вспомогательных функций, которые бесполезны вне очень специфического контекста. ;)
Мефан

16
@Thomas - вложенные функции могут быть отличным механизмом для разрушения сложных функций / алгоритмов, не заполняя текущую область видимости функциями, которые не имеют общего использования в пределах области видимости. Паскаль и Ада имеют (IMO) прекрасную поддержку для них. То же самое со 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
@ Томас Оуэнс: Хорошо, если вам нужна функция обратного вызова и вы не хотите загрязнять ею какое-то другое пространство имен.
Лев Дэвидсон

9
@Leo: Стандарт гласит, что для main есть две допустимые формы: int main()иint main(int argc, char* argv[])
John Dibling

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от меня.
ВОО

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

1
@Konrad: Другая проблема с ними заключается в том, что в C ++ 98 вы не должны использовать локальные типы в качестве параметров шаблона. Я думаю, что C ++ 1x снял это ограничение. (Или это был C ++ 03?)
sbi

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

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

42

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

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

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

Я не советую использовать это, это просто забавный трюк (может, но imho не должен).


2014 обновление:

С появлением C ++ 11 некоторое время назад вы можете иметь локальные функции, синтаксис которых немного напоминает JavaScript:

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

1
Должно быть operator () (unsigned int val), у вас пропущен набор скобок.
Джо Д

1
На самом деле, это вполне разумно, если вам нужно передать этот функтор в функцию или алгоритм stl, например std::sort(), или std::for_each().
Дима

1
@Dima: К сожалению, в C ++ 03 локально определенные типы не могут использоваться в качестве аргументов шаблона. C ++ 0x исправляет это, но также предоставляет гораздо более приятные решения лямбды, так что вы все равно этого не сделаете.
Бен Фойгт

Ой, ты прав. Виноват. Но, тем не менее, это не просто забавный трюк. Было бы полезно, если бы это было разрешено. :)
Дима

3
Рекурсия поддерживается. Однако вы не можете использовать autoдля объявления переменной. Страуструп дает пример: 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 ++ не поддерживает такие функции как стандарт. Большинство вариантов Паскаля и Ада делают (почти все языки на основе Алгола). То же самое с 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

Ах. У меня нет специальных расширений в моем компиляторе Си. Это приятно знать, хотя. Я добавлю этот ответ в свой ответ.
Томас Оуэнс

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

7

Все эти трюки выглядят (более или менее) как локальные функции, но они не работают так. В локальной функции вы можете использовать локальные переменные ее суперфункций. Это своего рода полуглобальные. Ни один из этих трюков не может этого сделать. Наиболее близким является лямбда-трюк из c ++ 0x, но его закрытие ограничено временем определения, а не временем использования.


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


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

(*) в мире C ++ использование макросов никогда не считается чистым.


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

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.