Evgenii Legotckoi
Evgenii LegotckoiТам. 6, 2018, 2:52 Т.Ж.

RAII идиомасы және құрылымдық бағдарламалаудан алынған функцияның бір кіру нүктесі және бір шығу нүктесі болуы керек деген мәлімдеме

Мазмұны

Мир программирования на C++ в новых стандартах позволяет нам вытворять самые разные вещи, благодаря которым можно спокойно отказываться от некоторых старых утверждений или принципов, либо просто гибко подходить к этим принципам.

Хотелось бы изложить свой взгляд на работу идиомы RAII и стандарта C++11 относительно одно устоявшийся принцип авторство которого приписывается Эдсгеру Дейкстре :

«модуль (в данном случае функция) должен иметь только одну точку входа и только одну точку выхода»

Для данного принципа множественные return в функции/методе противоречат принципам структурного программирования по следующим причинам:

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

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


Одними из таких средств в C++ могут быть идиома RAII , лямбда функции , и std::function из стандартной библиотеки.

RAII (Resource Acquisition Is Initialization) Получение ресурса есть инициализация - программная идиома объектно-ориентированного программирования, смысл которой заключается в том, что с помощью тех или иных программных механизмов получение некоторого ресурса неразрывно совмещается с инициализацией, а освобождение - с уничтожением объекта.

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

А сейчас посмотрим на доводы за и против использования множественных return.

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

Рассмотрим на примере

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

bool exampleFunction_1();
bool exampleFunction_2();

Главное функция, написанная по принципам структурного программирования

int examlpeFunctionMain()
{
    int result = 0;

    if (exampleFunction_1())
    {
        result = 1;
    }
    else if (exampleFunction_2())
    {
        result = 2;
    }

    return result;
}

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

Поэтому перепишем данный код на использование множественных return.

int examlpeFunctionMain()
{
    if (exampleFunction_1()) return 1;
    if (exampleFunction_2()) return 2;
    return 0;
}

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

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

Рассмотрим пример такого аргумента.

int examlpeFunctionMain()
{
    int result = 0;

    if (exampleFunction_1())
    {
        result = 1;
    }
    else if (exampleFunction_2())
    {
        result = 2;
    }

    std::cout << "Logging result " << result << std::endl;
    return result;
}

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

int examlpeFunctionMain()
{
    if (exampleFunction_1())
    {
        std::cout << "Logging result " << 1 << std::endl;
        return 1;
    }

    if (exampleFunction_2())
    {
        std::cout << "Logging result " << 2 << std::endl;
        return 2;
    }

    std::cout << "Logging result " << 0 << std::endl;
    return 0;
}

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

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

Но предлагаю теперь обратиться к современным возможностям языка программирования C++.

Идиом RAII подразумевает, что при уничтожении объекта в деструкторе можно освободить память, а также выполнить какой-нибудь программный код. Таким кодом может быть выполнение код логирования. Но у нас нет никаких подобных объектов? Да, на данный момент нет, но предлагаю написать класс для использования такого объекта.

Это будет шаблонный класс ScopExit. Рассмотрим его ниже.

#ifndef SCOPEEXIT_H
#define SCOPEEXIT_H

#include <functional>

class ScopeExit
{
public:
    template<typename T>
    explicit inline ScopeExit(T&& onScopeExitFunction) :
        m_onScopeExitFunction(std::forward<T>(onScopeExitFunction))
    {
    }

    inline ~ScopeExit()
    {
        m_onScopeExitFunction();
    }

private:
    std::function<void()> m_onScopeExitFunction;
};

#endif // SCOPEEXIT_H

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

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

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

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

int examlpeFunctionMain()
{
    int result = 0;
    ScopeExit scopeExit([&result](){ std::cout << "Logging result " << result << std::endl; });

    if (exampleFunction_1()) return (result = 1);
    if (exampleFunction_2()) return (result = 2);
    return 0;
}

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

Далее создаётся объект класса для ScopeExit, который при завершении метода будет уничтожен и вызовет в деструкторе лямбду.

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

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

Заключение

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

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

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Ол саған ұнайды ма? Әлеуметтік желілерде бөлісіңіз!

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
Г

C++ - Тест 001. Первая программа и типы данных

  • Нәтиже:66ұпай,
  • Бағалау ұпайлары-1
t

C++ - Тест 001. Первая программа и типы данных

  • Нәтиже:33ұпай,
  • Бағалау ұпайлары-10
t

Qt - Тест 001. Сигналы и слоты

  • Нәтиже:52ұпай,
  • Бағалау ұпайлары-4
Соңғы пікірлер
G
GoattRockҚыр. 3, 2024, 1:50 Т.Қ.
Linux жүйесінде файлдарды қалай көшіруге болады Задумывались когда-нибудь о том, как мы привыкли доверять свои вещи службам грузоперевозок? Сейчас такие услуги стали неотъемлемой частью нашей жизни, особенно когда речь идет о переездах между …
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssrАқп. 8, 2024, 6:43 Т.Қ.
Qt Linux - Сабақ 001. Linux астында Autorun Qt қолданбасы как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий КононенкоАқп. 5, 2024, 1:50 Т.Ж.
Qt WinAPI - Сабақ 007. Qt ішінде ICMP Ping арқылы жұмыс істеу Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
F
FynjyШілде 22, 2024, 4:15 Т.Ж.
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …
BlinCT
BlinCTМаусым 25, 2024, 1 Т.Ж.
Нарисовать кривую в qml Всем привет. Имеется Лист листов с тосками, точки получаны интерполяцией Лагранжа. Вопрос, как этими точками нарисовать кривую? ChartView отпадает сразу, в qt6.7 появился новый элемент…
BlinCT
BlinCTМамыр 5, 2024, 5:46 Т.Ж.
Написать свой GraphsView Всем привет. В Qt есть давольно старый обьект дял работы с графиками ChartsView и есть в 6.7 новый но очень сырой и со слабым функционалом GraphsView. По этой причине я хочу написать х…
Evgenii Legotckoi
Evgenii LegotckoiМамыр 2, 2024, 2:07 Т.Қ.
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.

Бізді әлеуметтік желілерде бақылаңыз