Evgenii Legotckoi
Evgenii Legotckoi6 августа 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 хостинг.

Вам это нравится? Поделитесь в социальных сетях!

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
SH
  • Sak Hax
  • 26 апреля 2024 г. 0:00

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

  • Результат:33баллов,
  • Очки рейтинга-10
г
  • ги
  • 24 апреля 2024 г. 1:51

C++ - Тест 005. Структуры и Классы

  • Результат:41баллов,
  • Очки рейтинга-8
l
  • laei
  • 23 апреля 2024 г. 19:19

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:10баллов,
  • Очки рейтинга-10
Последние комментарии
k
kmssr9 февраля 2024 г. 5:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 12:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 21:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 19:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 8:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
G
Gar22 апреля 2024 г. 15:46
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil Academics20 апреля 2024 г. 17:45
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasov14 апреля 2024 г. 16:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 12:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 14:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

Следите за нами в социальных сетях