3 декабря 2019 г.

Однократное выполнение кода

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

#define _ONCE(A) { static bool once__ = false; if (!once__) { once__ = true; A; } }

Развернём для удобства чтения:

#define _ONCE(A)                 \
{                                \
    static bool once__ = false;  \
    if (!once__)                 \
    {                            \
        once__ = true;           \
        A;                       \
    }                            \
}

Параметр A - это собственно код, который необходимо выполнить. Это может быть почти что угодно, например:

_ONCE(Print("rates_total=", rates_total))
_ONCE(show_some_debug_info())
_ONCE(
    if (a > b)
        Print("a > b");
)

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

{
    static int a = 1;
}

{
    static int a = 2;
}

Print(a); // ошибка, в этой области видимости `a` не существует

Так как каждый вызов макроса это подмена по шаблону, то в каждом таком месте будет своя область видимости и своя once_.

for (int i = 0; i < 10; i++)
{
    _ONCE(Print(i));
    _ONCE(Print(i));
}

После обработки макросов компилятором этот код превращается в:

for (int i = 0; i < 10; i++)
{
    {
        static bool once__ = false;
        if (!once__)
        {
            once__ = true;
            Print(i); // A
        }
    }
    {
        static bool once__ = false;
        if (!once__)
        {
            once__ = true;
            Print(i); // A
        }
    }
}

// Результат - только эти два нуля в логе:
// 0
// 0

Особенности и ограничения

Вложенный вызов _ONCE(_ONCE(...)) будет работать как без вложения, но компилятор предупредит о скрытии переменной.

Такой вызов будет преобразован в такой код:

{
    static bool once__ = false;
    if (!once__)
    {
        once__ = true;
        {
            // на это будет ругаться компилятор, т.к. в этой же области видимости 
            //   уже есть переменная с таким же именем
            static bool once__ = false;
            if (!once__)
            {
                once__ = true;
                ...
            }
        }
    }
}

Ещё пример, но теперь вложенные вызовы неявные:

void test1()
{
    _ONCE(Print("test1"); test2();); // once__ 3
}

void test2()
{
    _ONCE(Print("test2")); // once__ 4
}

void OnStart()
{
    _ONCE(test1(); test1();) // once__ 1 // второй test1() ничего не покажет, т.к. once__ 3 уже отметился
    _ONCE(test2(); test2();) // once__ 2 // ничего не покажет, т.к. once__ 4 уже отметился
}
// Результат:
// test1
// test2

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

class COnceTest
{
public:
    void test()
    {
        for (int i = 0; i < 10; i++)
            _ONCE(Print(i));
    }
};

void OnStart()
{
    COnceTest t1, t2;
    t1.test();
    t2.test();
}
// Результат:
// 0

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

Применение

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

Комментариев нет:

Отправка комментария