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

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

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
OI
  • Ora Iro
  • Жел. 24, 2024, 6:38 Т.Ж.

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

  • Нәтиже:40ұпай,
  • Бағалау ұпайлары-8
AD

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

  • Нәтиже:50ұпай,
  • Бағалау ұпайлары-4
m
  • molni99
  • Қаз. 26, 2024, 1:37 Т.Ж.

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

  • Нәтиже:80ұпай,
  • Бағалау ұпайлары4
Соңғы пікірлер
ИМ
Игорь МаксимовҚар. 22, 2024, 11:51 Т.Ж.
Django - Оқулық 017. Теңшелген Django кіру беті Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiҚаз. 31, 2024, 2:37 Т.Қ.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEҚаз. 19, 2024, 8:19 Т.Ж.
Qt Creator көмегімен fb3 файл оқу құралы Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовҚаз. 5, 2024, 7:51 Т.Ж.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Қар. 15, 2024, 6:04 Т.Ж.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectМаусым 4, 2022, 3:49 Т.Ж.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimҚаз. 25, 2024, 9:10 Т.Ж.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

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