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 хостинг.

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
AD

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

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 1:37

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

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 1:29

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

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 11:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 октября 2024 г. 14:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 8:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 7:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 11:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
m
moogo22 ноября 2024 г. 7:17
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii Legotckoi24 июня 2024 г. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 6:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 3:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

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