LINUX.ORG.RU

Покритикуйте подход: запуск функций из массива указателей

 


0

2

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

int sx1509_init( struct BicOS* os ){
	
	int res;
	int i;
	
	const struct {
		int (*function)( struct BicOS* );
		const char* name;
	} initSequence[] = {
		{ gpioSetup,                  "GPIO setup"                    },
		{ deviceSetup,                "Device setup"                  },
		{ interruptHandlerSetup,      "Interrupt handler setup"       },
		{ interruptEventHandlerSetup, "Interrupt event handler setup" },
		{ keyboardSetup,              "Keyboard setup"                },
		{ debug_key_printer_register, "Deug key printer"              },
		{ NULL,                       NULL                            }
	};
	
	for( i = 0; initSequence[i].function; i++ ){
		
		res = initSequence[i].function( os );
		if( res ){
			printf( "{B} [sx1509] %s failed.\n", initSequence[i].name );
			return -1;
		}
	}
	
	return 0;
}

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

★★

Последнее исправление: u5er (всего исправлений: 1)

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

cobold ★★★★★
()
Последнее исправление: cobold (всего исправлений: 1)
Ответ на: комментарий от u5er

Забудешься, сделаешь вот так добавляя новую функцию в список

		{ gpu_Setup,               NULL                            },
		{ NULL,                       NULL                            }

И получишь отвал письки :)

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)
Ответ на: комментарий от u5er

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

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

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 2)

Такое используется много где, в том числе видел в libc (только не помню от какой ОС) для организации всяких инициализаций перед вызовом main().

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

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

firkax ★★★★★
()

По сути ты сделал ивенты, ничего криминального в этом нет :)

Только я бы вывел собственно цикл вызовов и заполнение массива в отдельные функции (или даже файлы), там будет проще проверки делать.

Bfgeshka ★★★★★
()
Последнее исправление: Bfgeshka (всего исправлений: 1)

Ну, раз покритиковать, :)). Не, оверхеда тут нет, таблицы диспетчеризации применяются еще с тех пор когда языка С не было. А подвох в том что последовательность вызовов инициализации (а это серьезное дело, :)) ) при таком подходе жестко прописана, причем в локальном массиве. Что будет если после deviceSetup по какому то событию там надо будет вызвать не interruptHandlerSetup, а debug_key_printer_register, ну то есть сменить последовательность в run time? Таблицу тогда нужно сделать глобальной и реализовать какое то подобие state machine. Если же к алгоритму нет вопросов и не будет в будущем, и такая последовательность будет актуальной всегда, то да, код вполне себе.

spring
()

да всё ок..

ещё функций добавить в таблицу - де-инит в обратку :-) Или самим функциям параметр (init/destroy/reset/test...). И обратный цикл при ошибке.

а то сейчас если keyboardSetup сфейлил, interruptHandler остаётся висеть.

MKuznetsov ★★★★★
()

код называется - мама, смотри как я умею!

ну нравится делать так - делай, никто не запрещает. указатели на функции для такой вот дури и созданы.

alysnix ★★★
()

Здесь нет никакого подхода. Циклам 1000 лет. С таким же успехом можно рассказывать «я вместо такого f(0), f(1), f(2), ... сделал такое for (auto i : {0, 1, 2, ...}) f(i) - оцените подход».

anonymous
()

Вижу тема себя исчерпала, раз уж анонимусы нагрянули и начали нудеть). Спрошу здесь, чтоб не создавать новую тему. Благо эта тема аккумулировала всех сишников.

Есть программуля, в которой по переданным параметрам определяются действия которые нужно проделать над файлом. Тут логична самая простая реализация, по типу: проходим по дереву if-ов и на каждом этапе модификации файла дергаем нужную функцию с определенными параметрами. Но это громоздко и трудо-читаемо/понимаемо. Вот подумал, может это можно реализовать как-то красивее, при помощи указателей на функции? К примеру, каждую операцию над файлом в виде указателя на функцию закидывать в массив для обработки. И потом, точно так же, в for-е их скопом вызывать. Либо выделить переменную и устанавливать в ней флаги 0/1 в зависимости от условий, каждый из которых будет соответствовать конкретной операции. А потом прогнать эту переменную через case() и дернуть нужные функции.

Каким способом подобное оптимально было бы реализовать?

iron ★★★★★
()

Мне не нравится такой код. Не нужен тут цикл, просто напиши код как надо и всё. Ты ничего не выиграл, сделал код нечитаемым и сложным и усложнил работу оптимизатора. Также ты усложнил последующие доработки кода, когда тебе надо будет вставить что-то между этими вызовами или сделать менее тривиальную обработку ошибок. На мой взгляд это называется «изобретаем фреймворки на ровном месте». Не надо так.

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

vbr ★★★★
()
Последнее исправление: vbr (всего исправлений: 5)
Ответ на: комментарий от u5er

Это понятно. Но зачем? В чём цель избежать этого повторения? Что в нём плохого? Ты собираешься массово изменять сообщения об ошибках? Нет ничего плохого в повторении, как таковом. Особенно когда это повторение приводит к более простому коду.

Вообще 80% функций сводятся к тому, что они просто последовательно вызывают другие функции, проверяют их возвращаемое значение и возвращают управление в случае ошибки. Это значит, что каждую функцию надо писать через такой цикл указателей? Глупо ведь звучит.

Если хочется убрать повторение сообщения об ошибке, пиши код вида

res = gpioSetup(os);
if (res) {
   print_error(res, "GPIO setup");
   return -1;
}

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

А убирать повторения if - в этом смысле нет. Это ничего не даёт. Если тебе надо будет поменять if (res) на if (res != 0 && res != 1) то ты скорей всего сделаешь это в одном месте, а не во всех местах, а это уже как раз такое изменение, которое можно представить в будущем.

vbr ★★★★
()
Ответ на: комментарий от vbr

Что в нём плохого?

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

u5er ★★
() автор топика
Ответ на: комментарий от u5er

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

Ну напиши без пустых строк и в одну строку, если тебе это кажется более читаемым.

res = gpioSetup(os); if (res) { print_error("GPIO setup"); return -1; }

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

initSequence[] = {
  {
    .function = gpioSetup,
    .name = "GPIO setup"
  },
  {
    .function = deviceSetup,
    .name = "Device setup"
  },
  ...

И тут уже очевидно, что по коду получится плюс-минус то же.

vbr ★★★★
()
Последнее исправление: vbr (всего исправлений: 1)
Ответ на: комментарий от iron

Вот подумал, может это можно реализовать как-то красивее [..]

Не мала баба кропоту, тай купила порося.

В общем случае KISS. Читабельно? Правибельно? Ну и фиг, что громоздко. Зато через 10 лет сам себя материть не будешь.

beastie ★★★★★
()
Ответ на: комментарий от beastie

Читабельно?

Оно-то читабельно, но не понимабельно(. Так как распарсить 4-х этажные if-ы и понять что для чего там – не простая задача. Вот и думал, может есть какие-то более оптимальные подходы в подобных ситуациях.

iron ★★★★★
()
Ответ на: комментарий от iron

Вариант такой есть - создай словарь (дикт, хэш-таблицу). Заполняй словарь отдельно (например, через конфиг) соответствием расширения (или mime-типа) против выполняемой команды, если в рантайме соответствия не нашлось - пук в логи или в notify-send.

Bfgeshka ★★★★★
()
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define TRY(F, ARGS...) if (!F(ARGS)) {puts("Fail " #F "!!!"); abort();}

bool good() {
        return true;
}

bool bad(int n) {
        return false;
}

int main() {
        TRY(good);
        TRY(good);
        TRY(bad, 1);
        TRY(good);
        return 0;
}
./a.out 
Fail bad!!!
Аварийный останов
MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)

По сути ты сделал обычный сишный vtable. Вполне обычно и распространено.

Проблемы:

  1. Дай нормальное имя этой структуре и вынеси извне функции. За это тебе скажет спасибо тот, кто будет дебажить код в gdb;
  2. Неконсистентный стиль: keyboardSetup и debug_key_printer_register.
snizovtsev ★★★★★
()
Ответ на: комментарий от snizovtsev

Неконсистентный стиль: keyboardSetup и debug_key_printer_register.

Именно потому, что в коде используется camelCase, а нижние_подчёркивания используются для обозначения временного кода, который позже будет удалён.

u5er ★★
() автор топика
Ответ на: комментарий от snizovtsev

Значит глаз будет постоянно цепляться за эту функцию и рано или поздно она либо переименуется в debugKeyPrinterRegister или, что более вероятно, будет удалена.

Фишка в том, что сейчас я «портирую» драйвер микросхемы sx1509 с малинового wiringpi на esp’шную фряху, поэтому на данный момент задача стоит просто завести его. Потом всё это будет видоизменено. Вопрос темы не в этом.

u5er ★★
() автор топика

Поздравляю, ты сделал первый шаг к ООП.

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

hobbit ★★★★★
()
Ответ на: комментарий от hobbit

первый шаг к ООП

Больше похоже на шаг к функциональному программированию, функции как аргументы, еще чуть-чуть астрономических единиц, и функции - первоклассные объекты

anonymous
()
Ответ на: комментарий от hobbit

Поздравляю

Поздно, потому что я его сделал довольно давно, когда познакомился с программированием модулей ядра. В моём примере struct BicOS* os - это аналог вашего this. Вся разница лишь в том, что я явно передаю его в функцию, а у вас он просто есть.

u5er ★★
() автор топика
Ответ на: комментарий от u5er

Как вариант предлагаю макро:

bool foo() { return true; }
bool bar() { return true; }
bool baz() { return false; }

#define CHECK_TRUE(EXPR) if ( !(EXPR) ) { printf("failed: %s\n", #EXPR); return -1; }
int init()
{
    CHECK_TRUE(foo());
    CHECK_TRUE(bar());
    CHECK_TRUE(baz());
    return 0;
}
#undef CHECK_TRUE

https://godbolt.org/z/153Y4TW86

imatveev13
()
Последнее исправление: imatveev13 (всего исправлений: 2)
Ответ на: комментарий от cobold

Может обработка ошибок в самой функции прописана, допускаю такой вариант. А так, да, ошибки выводить надо.

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

Что думаете?

Fruct
()