Запуск нескольких функций с различной структурой аргумента/возврата по одному указателю функции

Правка: Я попробовал простой код в Arduino IDE, который должен был сделать в первую очередь вместо того, чтобы делать это в настольном компиляторе. Потому что результаты эксперимента отличаются. В настольном компиляторе C он работает и обходит код без особых проблем, всего одно или два предупреждения.

Но в Arduino IDE это неприемлемо и дает ошибку.

Вот этот код:

// functions
void function1(uint8_t x, uint8_t y){
  int z = x + y;
  Serial.println("function1\n");
  Serial.println(z);
}
uint8_t function2(uint8_t x, uint8_t y){
    Serial.println("function2\n");
    return 1;
}

void (*fun_ptr)();

void setup() {
  // put your setup code here, to run once:
  
  Serial.begin(9600);
  fun_ptr = (void (*)())function2;
  int value;
  
  value = fun_ptr(1,2);

}

void loop() {
  // put your main code here, to run repeatedly:

}

, 👍0

Обсуждение

Конечно, вы получите это предупреждение, так как fun_ptr - это указатель на функцию без каких-либо параметров, в то время как fun1 () - это функция с параметром uint8_t. Хотя мне непонятно, зачем вы это делаете, ведь вы хотите реализовать свой FSM с помощью корпуса переключателя. Какую структуру вы хотите построить с помощью указателя функции? Пожалуйста, покажите ему контекст вашего FSM, тогда мы сможем попытаться дать вам хороший ответ, @chrisl

Я отредактировал сообщение., @R1S8K

Обратите внимание, что единственное место, где вы вызываете fun_ptr (), вы не предоставляете никаких аргументов. Таким образом, для fun_ptr даже не имеет смысла ссылаться на функцию, которая действительно ожидает аргументов., @Edgar Bonet

Ладно, сейчас я пытаюсь исправить код, и спасибо за записку., @R1S8K

Я подумал, что могу перепроектировать вопрос со всех сторон, это хорошая идея или мне лучше начать новый вопрос ?, @R1S8K

тип " авто`, шаблоны/макросы и "пустые указатели" могут быть вашими лучшими друзьями в этом случае, @dandavis

да, "авто" - это тип C++, я фокусируюсь на C, я не знаю, почему я не хочу использовать C++ прямо сейчас ! Но что я думаю больше всего времени, так это то, что я хочу проверить все, что я могу сделать в C. Я хочу разработать диспетчер задач, теперь я понял, используя массив указателей функций + массив блокировок "bool" для каждого связанного числа указателей функций. Управляется в функции диспетчера задач., @R1S8K

Мне удалось вернуть данные из указателя функции void в функцию, которая возвращает "uint8_t", но теперь моя проблема связана с функцией, которая имеет несколько аргументов., @R1S8K


3 ответа


Лучший ответ:

1

Мне кажется, что ты пытаешься найти правильный ответ на неправильный вопрос, типичный для проблемы XY. В примере, приведенном в вашем собственном ответе, вы вызываете указатель функции сразу после установки его значения. В этом случае указатель на функцию бесполезен, так как вы можете напрямую вызвать либо read_chars (), либо write_chars().

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

void (*fun_ptr)();

void read_chars(uint8_t address) {
    printf("address of array is 0x%.2x\n", address);
}

void write_chars(uint8_t row, uint8_t col, const char *str) {
    printf("[%d, %d]: %s\n", row, col, str);
}

В одном разделе программы устанавливается указатель:

// Здесь мы узнаем, какое действие следует выполнить.
if (should_read()) {
    fun_ptr = (void(*)()) read_chars;
} else {  // should write
    fun_ptr = (void(*)()) write_chars;
}

и в другом разделе это называется:

// Здесь мы должны выполнить действие.
if (fun_ptr == (void(*)()) read_chars) {
    ((void(*)(uint8_t)) fun_ptr)(1);
} else if (fun_ptr == (void(*)()) write_chars) {
    ((void(*)(uint8_t, uint8_t, const char *)) fun_ptr)(
            1, 1, "messaage0");
}

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

enum {
    CALL_NONE, CALL_READ_CHARS, CALL_WRITE_CHARS
} what_to_call = CALL_NONE;

который будет использоваться следующим образом:

// Здесь мы узнаем, какое действие следует выполнить.
if (should_read()) {
    what_to_call = CALL_READ_CHARS;
} else {
    what_to_call = CALL_WRITE_CHARS;
}

// ...

// Здесь мы должны выполнить действие.
if (what_to_call == CALL_READ_CHARS)
    read_chars(1);
else if (what_to_call == CALL_WRITE_CHARS)
    write_chars(1, 1, "messaage1");

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

void (*fun_ptr)(void *);
void *fun_ptr_data;

void read_chars(void * data) {
    uint8_t address = * (uint8_t *) data;
    printf("address of array is 0x%.2x\n", address);
}

struct write_chars_data {
    uint8_t row;
    uint8_t col;
    const char *str;
};

void write_chars(void *data) {
    write_chars_data *d = (write_chars_data *) data;
    printf("[%d, %d]: %s\n", d->row, d->col, d->str);
}

Обратите внимание, что, поскольку write_chars() должен извлекать несколько параметров из одного указателя, он использует указатель на структуру.

Теперь часть кода, которая определяет, что следует делать, несет ответственность за предоставление данных:

// Здесь мы узнаем, какое действие следует выполнить.
if (should_read()) {
    fun_ptr = read_chars;
    uint8_t *p = new uint8_t;
    *p = 1;
    fun_ptr_data = (void *) p;
} else {  // should write
    fun_ptr = write_chars;
    write_chars_data *p = new write_chars_data;
    p->row = 1;
    p->col = 1;
    p->str = "messaage2";
    fun_ptr_data = (void *) p;
}

Часть кода, которая должна выполнить действие сейчас, очень проста:

// Здесь мы должны выполнить действие.
if (fun_ptr)
    fun_ptr(fun_ptr_data);

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

Этот шаблон довольно распространен в библиотеках, где клиент (пользователь библиотеки) хочет, чтобы было выполнено действие, но библиотека несет ответственность за выполнение этого действия в нужное время. Затем клиент предоставит библиотеке указатель на функцию, которая выполняет действие (называемое “обратным вызовом”), вместе с общим указателем на данные, необходимые для обратного вызова. Использование универсального указателя void * имеет смысл, поскольку библиотека не может знать, какие данные потребуются для обратного вызова.

В вашем случае, поскольку вы знаете, какие данные вам могут понадобиться, вы можете использовать указатель на объединение вместо общего указателя:

union fun_data {
    uint8_t address;  // для символов чтения
    struct {          // для символов записи
        uint8_t row;
        uint8_t col;
        const char *str;
    };
};

void (*fun_ptr)(fun_data *);
fun_data *fun_ptr_data;

Более идиоматичным стилем C++ было бы объединить функцию обратного вызова и ее данные в “функтор”, т. е. объект, который может быть вызван как функция, а также может хранить необходимые ему данные. Затем вы бы использовали наследование для создания определенных типов функторов при вызове универсального.

,

@R1S8K: 1. Вы можете заставить обратные вызовы возвращать номер, если хотите, но это должно быть сделано _ последовательно_: _ все обратные вызовы должны возвращать один и тот же тип, а указатель должен соответствовать их подписи. При назначении или использовании указателя функции вам не нужно _не_ вводить тип при назначении или использовании указателя функции. 2. Проблема с вашим ответом не в том, что он не работает. Дело в том, что это хрупкая и плохая практика. Будучи незаконным C++, он может очень хорошо работать на одной комбинации платформы/компилятора и потерпеть неудачу на следующей., @Edgar Bonet


2

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

То, что вы сейчас делаете, на самом деле не сработает. Когда вы вызываете функцию, вам необходимо указать ее аргументы. То же самое относится и к указателю на функцию. Вы не можете вызывать функцию, требующую параметров, без каких-либо параметров. Хотя вы все еще можете установить указатель функции типа (void (*)(void*) на функцию типа (void (*)(uint8_t)), компилятор предупреждает об этом, поскольку вы все равно не можете вызвать функцию без какого-либо параметра. Если вы попытаетесь это сделать, вы все равно получите сообщение об ошибке со слишком малым количеством аргументов для работы ....

Я вижу 2 пути отсюда:

  • Вы можете перегрузить свои функции, чтобы существовала версия без каких-либо параметров. Вот так:

      void foo(uint8_t i){Serial.println(i);}
      void foo(){foo(10);}
    

    При этом значение 10 является значением, с которым следует вызывать функцию, если вы не указываете параметры. Вы, конечно, можете также использовать переменные или вызовы функций здесь, чтобы фактически получить это значение для параметра i. Затем вы можете использовать указатель для вызова этой функции:

      void (* p)(void);
      p = &foo;
      p();
    

    Назначение указателя выберет версию функции без каких-либо параметров из перегруженного пула этой функции, поскольку она соответствует типу указателя. Обратите внимание, что я удалил * из определения параметра void. Я не понимаю, что бы это все равно значило (указатель на пустоту?). Указатель на функцию p() теперь можно вызывать без каких-либо параметров, так как он указывает на функцию без каких-либо параметров.

  • Вы можете отключить указатель на функцию и использовать простую инструкцию switch case с переменной состояния (которая содержит некоторый идентификатор для соответствующей функции), чтобы вызвать правильную служебную функцию. Я не буду здесь вдаваться в подробности, поскольку это уже в основном то, как FSM может быть запрограммирован на C++, о чем много раз говорится на этом сайте и в Интернете.

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

,

Re “_pointer to void?_”: пустота * обычно используется в качестве общего указателя, т. е. указателя на элемент данных неизвестного типа. См., например, [интерфейс qsort()](https://man7.org/linux/man-pages/man3/qsort.3p.html)., @Edgar Bonet


0

Вот моя последняя работа:

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

И изменил мою структуру данных, имеющую 2 вложенных структуры, на одну, которая содержит все, что должно хорошо служить моему проекту.

Вот моя работа:

task_manager.h

///////////////////////////////////////////////////////////////////////////
// enumerations
typedef enum {NOT_FINISHED,FINISHED,DONE}STATE;
// variables
typedef void (*f_ptr)(void *args);

// structs
typedef struct __attribute__((__packed__)) THREAD{
    uint8_t tsk_cnts, tsk_cntr;     // информация о задаче
    STATE   thrd_st;                  // состояния флага потока
    f_ptr   *tsk_fptr;
}THREAD;

main.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "task_manager.h"

const unsigned char PIC[] = {9, 8, 7, 1, 2};

void function1(void){printf("function1\n");}
void function2(uint8_t i){printf("function2\nnumber = %d\n",i);}
void function3(uint8_t a, uint8_t b, const uint8_t *img){
    printf("function3\na#%d b#%d\narray[0] = %d\n",a,b,*(img+0));}
uint8_t function4(uint8_t i){printf("function4# return\n"); return i+=1;}

THREAD thread1;

uint8_t fn4_ret;

int main(){

    // set thread main info
    thread1.thrd_st = 0;
    thread1.tsk_cnts = 4;
    thread1.tsk_cntr = 0;

    // allocating memory for tasks
    thread1.tsk_fptr = malloc(4*sizeof(f_ptr));

    // store tasks addresses
    thread1.tsk_fptr[0] = (void(*)())function1;
    thread1.tsk_fptr[1] = (void(*)())function2;
    thread1.tsk_fptr[2] = (void(*)())function3;
    thread1.tsk_fptr[3] = (void(*)())function4;

    // call the functions
    ((void(*)())thread1.tsk_fptr[1])(9);
    ((void(*)())thread1.tsk_fptr[2])(1,2,PIC);
    fn4_ret = ((uint8_t(*)())thread1.tsk_fptr[3])(1);
    printf("\fn4_ret = %d\n",fn4_ret);


    return 0;
}

Он прекрасно компилировался вместе со мной, передавал то, что я хочу, и возвращал необходимые данные.

,

Несмотря на то, что _may_ работает на некотором компиляторе/платформе, это незаконно в C++. gcc говорит о приведении: error: invalid conversion from ‘void (*)()’ to ‘uint8_t (*)()’, и о вызове через указатель: error: too many arguments to function`., @Edgar Bonet

Я отредактировал свой ответ. Что ты думаешь ? Это хорошо сработало со мной. Никаких проблем., @R1S8K