Почему я не могу определить функцию внутри другой функции?

Это не вопрос лямбда-функции, я знаю, что я могу назначить лямбда для переменной.

Какой смысл разрешить нам объявлять, но не определять функцию внутри кода?

Например:

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

Итак, что я хочу знать, почему С++ допускает two, который кажется бесполезным, и three, который кажется гораздо более сложным, но запрещает one?

EDIT:

Из ответов видно, что там, где объявление внутри кода может предотвратить загрязнение пространства имен, то, что я надеялся услышать, является причиной того, что возможность объявлять функции разрешена, но способность определять функции была запрещена.

Ответ 1

Непонятно, почему one не разрешено; вложенные функции были предложены давно в N0295, в котором говорится:

Мы обсуждаем введение вложенных функций в С++. Вложенные функции хорошо поняты и их введение мало усилий со стороны поставщиков компиляторов, программистов или комитета. Вложенные функции предоставляют значительные преимущества, [...]

Очевидно, что это предложение было отклонено, но поскольку в течение 1993 у нас нет минут собрания, доступных в Интернете, у нас нет возможного источника для обоснования этого отклонения.

Фактически это предложение отмечено в Лямбда-выражениях и замыканиях для C ++ в качестве возможной альтернативы:

Одна статья [Bre88] и предложение N0295 на C ++ комитет [SH93] предлагает добавить вложенные функции в C ++. Вложенные функции аналогичны лямбда-выражениям, но определяются как выражения внутри тела функции, а полученные закрытие не может быть использовано, если эта функция не активна. Эти предложения также не включают добавление нового типа для каждого лямбда-выражения, но вместо этого реализуя их больше как обычные функции, в том числе позволяя специальному указателю функции ссылаться на них. Оба эти предложения предшествуют добавлению шаблонов к C ++, и поэтому не упоминайте использование вложенных функций в сочетании с общими алгоритмами. Кроме того, эти предложения не имеют возможности скопировать локальные переменные в замыкание, поэтому вложенные функции они продукция полностью непригодна для использования вне их закрывающей функции

Учитывая, что у нас теперь есть lambdas, мы вряд ли увидим вложенные функции, поскольку, как обрисовывает статья, они являются альтернативами одной и той же проблемы, а вложенные функции имеют несколько ограничений относительно lambdas.

Что касается этой части вашего вопроса:

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

Есть случаи, когда это было бы полезным способом вызвать нужную функцию. Проект стандартного раздела С++ 3.4.1 [basic.lookup.unqual] дает нам один интересный пример:

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

Ответ 2

Ну, ответ - "исторические причины". В C вы могли иметь объявления функций в области блока, а разработчики С++ не видели преимущества в удалении этой опции.

Пример использования:

#include <iostream>

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

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

IMO это плохая идея, потому что легко сделать ошибку, предоставив объявление, которое не соответствует реальному определению функции, что приводит к поведению undefined, которое не будет диагностировано компилятором.

Ответ 3

В примере, который вы даете, void two(int) объявляется как внешняя функция, причем это объявление является действительным только в пределах области main.

Это разумно, если вы хотите сделать имя two доступным в main(), чтобы избежать загрязнения глобального пространства имен в текущем блоке компиляции.

Пример в ответ на комментарии:

main.cpp:

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

foo.cpp:

int foo() {
  return 0;
}

нет необходимости в файлах заголовков. скомпилировать и связать с

c++ main.cpp foo.cpp 

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

Ответ 4

Вы можете делать все это во многом потому, что на самом деле это не так сложно сделать.

С точки зрения компилятора, объявление функции внутри другой функции довольно тривиально для реализации. Компилятору нужен механизм, позволяющий декларациям внутри функций обрабатывать другие объявления (например, int x;) внутри функции в любом случае.

Обычно он будет иметь общий механизм для разбора объявления. Для парня, пишущего компилятор, на самом деле не имеет значения, запускается ли этот механизм при анализе кода внутри или за пределами другой функции - это просто объявление, поэтому, когда он видит достаточно, чтобы знать, что есть объявление, он вызывает часть компилятора, который обрабатывает объявления.

Фактически, запрет на эти конкретные объявления внутри функции, вероятно, добавит дополнительной сложности, потому что компилятору понадобится совершенно безвозмездная проверка, чтобы проверить, не ищет ли он код внутри определения функции и на основании этого решает, разрешать или разрешать запретить это конкретное выражение.

Это оставляет вопрос о том, как вложенная функция отличается. Вложенная функция отличается тем, что она влияет на формирование кода. В языках, допускающих вложенные функции (например, Pascal), вы обычно ожидаете, что код в вложенной функции имеет прямой доступ к переменным функции, в которую он вложен. Например:

int foo() { 
    int x;

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

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

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

Компилятор (при условии, что он не оптимизирует лишний код) может генерировать код, который примерно эквивалентен этому:

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

В частности, он имеет одно местоположение, указывающее на начало блока локальных переменных, и весь доступ к локальным переменным является смещением от этого местоположения.

С вложенными функциями, которые больше не являются случаем, вместо этого функция имеет доступ не только к своим локальным переменным, но и к переменным, локальным для всех функций, в которые он вложен. Вместо того, чтобы просто иметь один "стек_поинтер", из которого он вычисляет смещение, ему нужно пройти резервную копию стека, чтобы найти локаторы стека локально для функций, в которые он вложен.

Теперь, в тривиальном случае, не все, что ужасно - если bar вложен внутри foo, тогда bar может просто искать стек в предыдущем указателе стека для доступа к переменным foo, Правильно?

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

Это, однако, означает, что доступ к "локальной" переменной не может быть тривиальным вопросом. Поиск правильного стека кадров для доступа к переменной может быть нетривиальным, поэтому доступ к переменным окружающих функций также (по крайней мере, обычно) медленнее, чем доступ к действительно локальным переменным. И, конечно же, компилятор должен сгенерировать код для поиска правильных кадров стека, доступа к переменным через любое произвольное количество кадров стека и т.д.

Это сложность, которую избегал C, запрещая вложенные функции. Теперь, конечно, верно, что текущий компилятор С++ - это совсем другой вид зверя из компилятора C v 1970 года. С такими вещами, как множественное виртуальное наследование, компилятор С++ должен иметь дело с вещами по этому же общему характеру в любом случае (т.е. Найти расположение переменной базового класса в таких случаях также может быть нетривиальным). На процентном основании поддержка вложенных функций не добавила бы большой сложности текущему компилятору С++ (а некоторые, например, gcc, уже поддерживают их).

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

Нижняя строка: хотя вначале казалось, что вложенные объявления жесткие, а вложенные функции тривиальны, более или менее противоположное значение истинно: вложенные функции на самом деле намного сложнее для поддержки, чем вложенные объявления.

Ответ 5

Первое - это определение функции, и это недопустимо. Очевидно, wt - использование определения функции внутри другой функции.

Но другие два являются просто декларациями. Представьте, что вам нужно использовать функцию int two(int bar); внутри основного метода. Но он определен ниже функции main(), поэтому объявление функции внутри функции позволяет использовать эту функцию с объявлениями.

То же самое относится к третьему. Объявления класса внутри функции позволяют вам использовать класс внутри функции без предоставления соответствующего заголовка или ссылки.

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

Ответ 6

Эта языковая функция была унаследована от C, где она служила какой-то цели в начале C (возможно, объявление определения функции)?. Я не знаю, сильно ли эта функция используется современными программистами C, и я искренне сомневаюсь в этом.

Итак, подведем итог:

нет цели для этой функции в современном С++ (что я знаю, по крайней мере), она здесь из-за обратной совместимости С++-to-C (я полагаю:)).


Благодаря комментарию ниже:

Прототип функции привязан к функции, в которой он объявлен, поэтому можно иметь более крутое глобальное пространство имен - обращаясь к внешним функциям/символам без #include.

Ответ 7

На самом деле, есть один вариант использования, который, по-видимому, полезен. Если вы хотите, чтобы определенная функция вызывалась (и компилируется ваш код), независимо от того, что объявляет окружающий код, вы можете открыть свой собственный блок и объявить в нем прототип функции. (Источником вдохновения является Johannes Schaub, fooobar.com/questions/15303/..., через TeKa, fooobar.com/questions/15305/...).

Это может быть особенно полезно, если вам нужно включить заголовки, которые вы не контролируете, или если у вас многострочный макрос, который может использоваться в неизвестном коде.

Ключ состоит в том, что локальная декларация заменяет предыдущие объявления в самом внутреннем блоке. Хотя это может ввести тонкие ошибки (и, я думаю, запрещено на С#), его можно использовать сознательно. Рассмотрим:

// somebody header
void f();

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

Связывание может быть интересным, поскольку вероятность того, что заголовки принадлежат библиотеке, но я думаю, вы можете настроить аргументы компоновщика, чтобы f() был разрешен к вашей функции к моменту рассмотрения библиотеки. Или вы говорите ему, чтобы игнорировать повторяющиеся символы. Или вы не связываетесь с библиотекой.

Ответ 8

Это не ответ на вопрос ОП, а ответ на несколько комментариев.

Я не согласен с этими моментами в комментариях и ответах: 1, что вложенные объявления якобы безвредны, а 2, что вложенные определения бесполезны.

1 Основной контрпример для предполагаемой безвредности объявлений вложенных функций - печально известный Most Vexing Parse. ИМО распространение путаницы, вызванное этим, достаточно, чтобы гарантировать дополнительное правило, запрещающее вложенные декларации.

2 1-й контрпример к предполагаемой бесполезности определений вложенных функций часто требуется выполнять одну и ту же операцию в нескольких местах внутри одной функции. Для этого существует очевидное обходное решение:

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

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

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

Ответ 9

В частности, отвечая на этот вопрос:

Из ответов видно, что там, где объявление внутри кода может предотвратить загрязнение пространства имен, то, что я надеялся услышать, является причиной того, что возможность объявлять функции разрешена, но способность определять функции была запрещена.

Потому что рассмотрим этот код:

int main()
{
  int foo() {

    // Do something
    return 0;
  }
  return 0;
}

Вопросы для разработчиков языка:

  • Должно ли foo() быть доступным для других функций?
  • Если да, то каково должно быть его имя? int main(void)::foo()?
  • (Обратите внимание, что 2 не будет возможным в C, создателе С++)
  • Если нам нужна локальная функция, у нас уже есть способ - сделать ее статическим членом локально определенного класса. Итак, следует ли добавить еще один синтаксический метод достижения одного и того же результата? Зачем это? Разве это не увеличило бы нагрузку на содержание разработчиков компилятора С++?
  • И так далее...

Ответ 10

Просто хотел указать, что компилятор GCC позволяет вам объявлять функции внутри функций. Подробнее об этом здесь. Также с введением lambdas в С++ этот вопрос сейчас немного устарел.


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

void do_something(int&);

int main() {
    int my_number = 10 * 10 * 10;
    do_something(my_number);

    return 0;
}

void do_something(int& num) {
    void do_something_helper(int&); // declare helper here
    do_something_helper(num);

    // Do something else
}

void do_something_helper(int& num) {
    num += std::abs(num - 1337);
}

Что у нас есть? В принципе, у вас есть функция, которая должна быть вызвана из main, поэтому вы делаете то, что вы отправляете объявление, как обычно. Но тогда вы понимаете, что эта функция также нуждается в другой функции, чтобы помочь ей в том, что она делает. Поэтому вместо того, чтобы объявлять эту вспомогательную функцию выше основной, вы объявляете ее внутри функции, которая ей нужна, а затем ее можно вызывать из этой функции и только этой функции.

Моя мысль заключается в том, что объявление функций внутри функций может быть косвенным методом инкапсуляции функций, что позволяет функции скрывать некоторые части того, что она делает, делегируя какую-то другую функцию, о которой только она знает, почти давая иллюзию вложенной функции.

Ответ 11

Вложенные объявления функций допускаются, вероятно, для 1. Прямые ссылки 2. Уметь объявлять указатель на функции и передавать другие функции в ограниченной области.

Вложенные определения функций не допускаются, вероятно, из-за таких проблем, как 1. Оптимизация 2. Рекурсия (охватывающая и вложенная определенная функция (ы)) 3. Повторное вмешательство 4. Concurrency и другие проблемы с многопоточным доступом.

Из моего ограниченного понимания:)