Подтвердить что ты не робот

Вложенные функции - это плохо в gcc?

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

Это плохо? Если да, не могли бы вы показать мне несколько неприятных примеров? Каков статус вложенных функций в gcc? Они собираются быть удалены?

4b9b3361

Ответ 1

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

Небольшая история, чтобы проиллюстрировать это - я работал на политехникум из Великобритании, который в основном использовал DEC-боксы - в частности, DEC-10 и некоторые VAXen. Весь инженерный факультет использовал в своем коде множество расширений DEC для FORTRAN - они были уверены, что мы останемся магазином DEC навсегда. А затем мы заменили DEC-10 на мэйнфрейм IBM, компилятор FORTRAN которого не поддерживает ни одно из расширений. В тот день было много воплей и скрежетов зубов, могу вам сказать. Мой собственный код FORTRAN (симулятор 8080) был перенесен в IBM за пару часов (почти все занялись изучением того, как управлять компилятором IBM), потому что я написал его в стандартном болоте FORTRAN-77.

Ответ 2

Иногда вложенные функции могут быть полезны, особенно с алгоритмами, которые перемешиваются вокруг множества переменных. Что-то вроде выписанного 4-way merge sort могло бы содержать множество локальных переменных и иметь несколько фрагментов повторяющегося кода, которые используют многие из них. Вызов этих битов повторного кода в качестве внешней вспомогательной подпрограммы потребует передачи большого количества параметров и/или наличия вспомогательной процедуры доступа к ним через другой уровень указателя.

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

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

Ответ 3

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

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

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

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

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

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

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

Переносимость важна, но gcc доступен во многих средах, и, по крайней мере, еще одно семейство компиляторов поддерживает вложенные функции - IBM xlc, доступные в AIX, Linux на PowerPC, Linux на BlueGene, Linux on Cell и z/OS. Видеть  http://publib.boulder.ibm.com/infocenter/comphelp/v8v101index.jsp?topic=%2Fcom.ibm.xlcpp8a.doc%2Flanguage%2Fref%2Fnested_functions.htm

Вложенные функции доступны в некоторых новых (например, Python) и многих других традиционных языках, включая Ada, Pascal, Fortran, PL/I, PL/IX, Algol и COBOL. С++ даже имеет две ограниченные версии: методы в локальном классе могут обращаться к своей содержащей функции статическим (но не авто) переменным, а методы в любом классе могут обращаться к элементам и методам статического класса. Предстоящий стандарт С++ имеет функции lamda, которые действительно являются анонимными вложенными функциями. Таким образом, мир программирования обладает большим опытом и поддержкой.

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

Ответ 4

Как вы сказали, это плохо в том смысле, что они не являются частью стандарта C и как таковые не реализованы многими (любыми?) другими компиляторами C.

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

Ответ 5

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

  • Используются GCC и вложенные функции

  • используется указатель на вложенную функцию

  • вложенная функция обращается к переменным из родительской функции

  • архитектура предлагает защиту от NX (без выполнения), например, 64-разрядный Linux.

При выполнении вышеуказанных условий GCC создаст батут https://gcc.gnu.org/onlinedocs/gccint/Trampolines.html. Чтобы поддерживать батуты, стек будет отмечен как исполняемый файл. см. https://www.win.tue.nl/~aeb/linux/hh/protection.html

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

Ответ 6

Поздно к партии, но я не согласен с принятым утверждением ответа, что

Вложенные функции действительно не делают ничего, что вы не можете сделать с не вложенные.

В частности:

TL; DR: Вложенные функции могут уменьшить использование стека во встроенных средах

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

void do_something(my_obj *obj) {
    double times2() {
        return obj->value * 2.0;
    }
    double times4() {
        return times2() * times2();
    }
    ...
}

Обратите внимание, что после того, как вы находитесь внутри do_something(), из-за вложенных функций вызовы times2() и times4() не нуждаются в том, чтобы вставлять какие-либо параметры в стек, просто возвращать адреса (а интеллектуальные компиляторы даже оптимизировать если возможно).

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

Ответ 7

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

Я также предлагаю вам редко использовать вложенные встроенные функции редко, и несколько раз, когда вы их используете, вы должны иметь (в своем уме и в некоторых комментариях) стратегию избавиться от них (возможно, даже реализовать ее с условными #ifdef __GCC__).

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

Другое расширение GCC, на мой взгляд, очень полезно, это вычисленный goto, т.е. label как значения. При кодировании автоматов или интерпретаторов байт-кода это очень удобно.

Ответ 8

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

С другой стороны, они не переносимы на другие компиляторы. (Компиляторы примечаний, а не устройства. Не так много мест, где gcc не запускается).

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

Ответ 9

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

Ответ 10

Я просто изучаю немного другое использование вложенных функций. В качестве подхода для "ленивой оценки" в C.

Представьте такой код:

void vars()
{
  bool b0 = code0; // do something expensive or to ugly to put into if statement
  bool b1 = code1;

  if      (b0) do_something0();
  else if (b1) do_something1();
} 

против

void funcs()
{
  bool b0() { return code0; }
  bool b1() { return code1; }

  if      (b0()) do_something0();
  else if (b1()) do_something1();
}

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

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

Ответ 11

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

У меня есть объекты, которые присматривают за различными аппаратными устройствами. Это структуры, которые передаются по указателю в качестве параметров функциям-членам, а не так, как это происходит автоматически в c++.

Так что я мог бы иметь

    static int ThisDeviceTestBram( ThisDeviceType *pdev )
    {
        int read( int addr ) { return( ThisDevice->read( pdev, addr ); }
        void write( int addr, int data ) ( ThisDevice->write( pdev, addr, data ); }
        GenericTestBram( read, write, pdev->BramSize( pdev ) );
    }

GenericTestBram не знает и не может знать об ThisDevice, который имеет несколько экземпляров. Но все, что ему нужно, это средства для чтения и письма и размер. ThisDevice-> read (...) и ThisDevice-> Write (...) нужен указатель на ThisDeviceType для получения информации о том, как читать и записывать память блока (Bram) этого конкретного экземпляра. Указатель pdev не может иметь глобальную область действия, поскольку существует несколько экземпляров, и они могут выполняться одновременно. Поскольку доступ осуществляется через интерфейс FPGA, это не простой вопрос передачи адреса, и он варьируется от устройства к устройству.

Код GenericTestBram - это служебная функция:

    int GenericTestBram( int ( * read )( int addr ), void ( * write )( int addr, int data ), int size )
    {
        // Do the test
    }

Поэтому тестовый код должен быть написан только один раз и не должен знать подробности структуры вызывающего устройства.

Однако даже с GCC вы не можете этого сделать. Проблема в том, что указатель находится вне области видимости, и эта проблема должна быть решена. Единственный известный мне способ сделать f (x,...) неявно осведомленным о его родителе - это передать параметр со значением вне диапазона:

     static int f( int x ) 
     {
         static ThisType *p = NULL;
         if ( x < 0 ) {
             p = ( ThisType* -x );
         }
         else
         {
             return( p->field );
         }
    }
    return( whatever );

Функция f может быть инициализирована чем-то, что имеет указатель, а затем вызываться из любого места. Не идеально, хотя.

Ответ 12

Вложенные функции являются MUST-HAVE на любом серьезном языке программирования.

Без них фактическое чувство функций неприменимо.

Он называется лексическим охватом.