Невозможность реализации API. Следствие монолитной архитектуры: использование API предполагает возможность модификации поведения исполнителя без изменения кода инициатора. Поскольку они оба связаны через единый объект, выполнение указанного требования является нереализуемым.
Высокое быстродействие. А вот здесь недостатки монолитной архитектуры превращаются в достоинства. Дело в том, что поскольку инициатор сохраняет у себя объект, он имеет доступ к коду перегруженного оператора, т. е. к коду обработчика вызова. Как следствие, оптимизирующий компилятор получает возможность встроить код обработчика непосредственно в точку вызова, опуская вызов функции (перегруженный оператор тоже является функцией), что значительно ускоряет выполнение вызова. Рассмотрим этот момент подробнее.
2.4.6. Производительность
С точки зрения машинных команд, вызов функции – не слишком быстрая операция. Необходимо несколько команд для сохранения стека10; команда перехода к коду функции; команда возврата управления; несколько команд для восстановления стека. А если код тела функции небольшой, к примеру, всего лишь сравнение двух величин, то время, затраченное на вызов функции, может значительно превысить время выполнения кода функции.
Поясним сказанное на примере. Напишем маленькую простую программу, которая считывает из консоли два числа, складывает их и результат выводит на экран (Листинг 19).
#include <iostream>
int Calculate(int a, int b)
{
return a + b;
}
int main()
{
int a, b;
std::cin >> a >> b;
int result = Calculate(a, b);
std::cout << result;
}
Откомпилируем код с выключенной оптимизацией и запустим на выполнение. Посмотрим дизассемблерный участок кода 11, в котором производится вызов функции (Листинг 20):
int Calculate(int a, int b)
{
00007FF6DA741005 and al,8 // 1
return a + b;
00007FF6DA741008 mov eax,dword ptr [b] // 2
00007FF6DA74100C mov ecx,dword ptr [a] // 3
00007FF6DA741010 add ecx,eax // 4
00007FF6DA741012 mov eax,ecx // 5
}
00007FF6DA741014 ret // 6
int main()
{
…….
int result = Calculate(a, b);
00007FF6DA741053 mov edx,dword ptr [b] // 7
00007FF6DA741057 mov ecx,dword ptr [a] // 8
00007FF6DA74105B call Calculate (07FF6DA741000h) // 9
00007FF6DA741060 mov dword ptr [result],eax // 10
…….
В строках 7 и 8 введенные значения a и b сохраняются в регистрах. В строке 9 выполняется вызов функции. В строке 1 выполняется обнуление результата, в строках 2 и 3 переданные значения копируются в регистры, в строке 4 выполняется сложение, в строке 5 результат копируется обратно в регистр, в строке 6 выполняется выход из функции, в строке 10 результат вычисления функции копируется в переменную результата.
Теперь включим оптимизацию, откомпилируем и посмотрим на код (Листинг 21):
int main()
{
…….
int result = Calculate(a, b);
00007FF7D5B11033 mov edx,dword ptr [b]
00007FF7D5B11037 add edx,dword ptr [a]
Как видим, для вычислений у нас всего две операции: запись в регистр значения b и добавление к нему значения a. Код встроен в поток выполнения, вызов функции не производится. Ощутимая разница, не правда ли?
2.5. Лямбда-выражение
2.5.1. Концепция
Лямбда-выражение12 – это локальная неименованная функция, которая, подобно обычной функции, может принимать входные параметры и возвращать результат. Особенностью лямбда-выражений, отличающих их от обычных функций, является возможность захвата переменных.
Графическое изображение обратного вызова с помощью лямбда-выражения представлено на Рис. 15. Исполнитель реализуется в виде какой-либо исполняемой функции, в качестве которой могут выступать глобальная функция, статический метод класса, метод-член класса, перегруженный оператор. Код обратного вызова упаковывается в лямбда-выражение, в качестве контекста выступают захваченные переменные. При настройке лямбда-выражение как аргумент сохраняется в инициаторе. Инициатор осуществляет обратный вызов посредством вызова хранимого выражения, передавая ему требуемую информацию. Контекст здесь передавать не нужно, поскольку внутри тела лямбда-выражения доступны все захваченные переменные.
11
Этот код получен с помощью компилятора Microsoft Visual studio версии 19.23.28106.4. Другие компиляторы могут генерировать отличающийся код, но принцип останется прежним.
12
В литературе можно встретить термин «лямбда-функция», но в стандарте С++ он именуется как “lambda-expression”, что в переводе означает «лямбда-выражение».