Вложенные функции на C++

в 18:38, , рубрики: c++, function, functor, macros, метки: , , ,

Приветствую сообщество!

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

#include <iostream>
int main()
{
    inline_function(std::string s)
    {
        std::cout << "Hello, " << s << "!n";
    }
    with_name(make_hello);

    make_hello("Vasiliy Pupkin!");
    return 0;
}

В приведенном примере внутри метода main изготавливается вложенный «метод» с названием make_hello и затем вызывается с параметром «Vasiliy Pupkin». Разумеется, на экран будет выведено Hello, Vasiliy Pupkin!.

К сожалению, перетащить название вверх у меня не получилось.

Сделано это, конечно же, на макросах.

Примеры того же самого без макросов

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

Ответа целых два: это, во-первых, вызов конструктора класса без с созданием объекта «в пустоту»:

#include <stdio.h>
int main()
{
    // Вместо функции мы изготовим класс с конструктором
    class inline_function 
    {
        public:
        // Вызов конструктора будет синтаксически неотличим от вызова вложенного метода
        inline_function() 
        {
            printf("Hello, hell!n");
        }
    };
    
    // создание объекта и вызов конструктора
    inline_function();
    return 0;
}

К сожалению, есть два существенных «но», которые не позволяют использовать приведенный пример на практике:

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

2. Visual Studio со включенной оптимизацией компилятора просто-напросто вырежет создание inline_function() как бесполезное. Логика компилятора понятна (все равно этот создаваемый объект никто не будет использовать, так зачем его создавать?) но представляет опасность.

Однако есть еще штука под названием «переопределение операторов» — и мы можем переопределить двойные скобочки (по умному переопределенный оператор () называется «функтором», во как):

#include <stdio.h>
int main()
{
    // Вместо функции мы изготовим класс переопределенным оператором
    class inline_function_class 
    {
        public:
            void operator()()
            {
                printf("Hello, world!n");
            }
    };
    // Создадим объект только что созданного типа -- всего один раз!
    inline_function_class inline_function; 

    // И вызовем функтор
    inline_function(); 
    return 0;
}

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

#include <stdio.h>
int main()
{
    // Вместо функции мы изготовим класс переопределенным оператором
    class  
    {
        public:
            void operator()()
            {
                printf("Hello, world!n");
            }
    } inline_function; // Декларация и объявление в одном флаконе

    // И вызовем функтор
    inline_function(); 
    return 0;
}

Пишем макросы

Итак, цель — в принципе — достигнута: у нас есть способ изолировать кусок кода внутри одного метода так, чтобы не подпортить внешнюю зону видимости. Но есть один минус: во всех приведенных примерах — на мой вкус — слишком много букв. Поэтому мы разделим наш код на три части:

#include <stdio.h>
int main()
{
    // Вместо функции мы изготовим класс переопределенным оператором
    /** первая часть -- до имплементации функции, которая заранее известна, если не говорить о параметрах: **/
    class  
    {
        public:
            void operator()(/* а тут ведь могут быть и параметры*/)
            {
                /**вторая часть -- собственно, тело функции, которое будет писать человек **/
                printf("Hello, world!n");
               /** Третья часть -- завершающая часть класса **/
            }
    } inline_function; // Декларация и объявление в одном флаконе
    /** =============================== **/
    // И вызовем функтор
    inline_function(); 
    return 0;
}

Первую и вторую части можно запихнуть в два макроса, дав им какие-нибудь красивые имена. Например,

#define inline_function(params) 
class 
{ 
    public: void operator() (params)
    {

#define with_name(value) 
    }
} value;

#define with_params(...) __VA_ARGS__ // А это-то зачем тут? Читай ниже.

При помощи этих макросов код «вложенной функции» значительно упрощается на вид (хоть и выглядит не в С++ стиле):

int main()
{
  inline_function(char * name)
  {
    printf("Hello, %s!n",name);
  } with_name(hello)

  hello("Pupkin");
}

К сожалению, все портится, если попробовать задать не один параметр у функции, а хотя бы два. В этом случае компилятор нажалуется, мол, в макросе inline_function всего ОДИН параметр и баста. Проблему можно решить, использовав макрос __VA_ARGS__ (который, хоть и не входит в стандарт, но поддерживается всеми реально используемыми компиляторами).

Чуть сложнее, но все еще приемлимо будет выглядеть функция с двумя параметрами:

int main()
{
  inline_function (with_params(int a, int b))
  {
    printf("%d+%d=%dn",a,b,a+b);
  } with_name(plus);

  plus(2,2);
}

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

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

Автор: Arenim


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js