Как называется этот метод и нарушает ли он строгие правила псевдонимов или вызывает UB?

Я придумал код, который использует самореферентную структуру (первый элемент структуры — это указатель на функцию, которая принимает экземпляр структуры в качестве своего единственного аргумента).

Было полезно передавать разрозненные подпрограммы другой для вызова, потому что вызывающей подпрограмме не нужно знать точный состав аргументов переданных подпрограмм (см. process_string места вызова в коде ниже). Сами переданные/вызванные подпрограммы отвечают за распаковку (приведение) аргументов осмысленным для них способом.

В нижней части этого поста приведен пример кода, использующего эту технику. При компиляции с gcc -std=c99 -Wpedantic -Wall -Wextra -Wconversion он производит следующий вывод:

nread: 5
vals[0]: 0.000000
vals[1]: 0.000000
vals[2]: 0.000000
vals[3]: 78.900000
vals[4]: 32.100000
vals[5]: 65.400000
vals[6]: 87.400000
vals[7]: 65.000000
12.3 12.3
34.5 34.5
56.7 56.7
78.9 78.9
32.1 32.1
65.4 65.4
87.4 87.4
65.0 65.0

Мои вопросы:

  1. Как называется эта техника? Как видно из кода, я использовал имя функтор, но я не уверен, что это правильно. Это немного похоже на замыкание, но я не думаю, что это так, поскольку оно просто указывает на свои аргументы, а не копирует их.
  2. Нарушает ли код строгое правило псевдонимов?
  3. Вызывает ли код Undefined Behavior?

А теперь по коду:

#include <stdio.h>

typedef struct functor_s functor_t;
typedef int (func_t)(functor_t);
struct functor_s { func_t * _0; void * _1; void * _2; void * _3; void * _4; };

void process_string(char * buf, int skip, functor_t ftor) {
    for (int i = skip; i < 8; ++i) {
        ftor._4 = buf + i*5;
        ftor._3 = &i;
        (void)ftor._0(ftor);
    }
}

int scan_in_double(functor_t in) {
    // unpack the args
    const char * p = in._4;
    int offset = *(int*)in._3;
    int * count = in._1;
    double * dest = in._2;

    // do the work
    return *count += sscanf(p, "%lg", dest + offset);
}

int print_repeated(functor_t in) {
    // unpack the args
    const char * p = in._4;
    
    // do the work
    char tmp[10] = {0};
    sscanf(p, "%s", tmp);
    printf("%s %s\n", tmp, tmp);
    return 0;
}

int main()
{
    char line[50] = "12.3 34.5 56.7 78.9 32.1 65.4 87.4 65.0";

    int nread = 0;
    double vals[8] = {0};

    functor_t ftor1 = { scan_in_double, &nread, vals };
    process_string(line, 3, ftor1);

    // check that it worked properly
    printf("nread: %d\n", nread);
    for (int i = 0; i < 8; ++i) {
        printf("vals[%d]: %f\n", i, vals[i]);
    }
    
    functor_t ftor2 = { print_repeated };
    process_string(line, 0, ftor2);

    return 0;
}

EDIT: в ответ на предложение @supercat (https://stackoverflow.com/a/63332205/1206102) я переработал мой пример для передачи двойного косвенного указателя на функцию (который, кстати, сделал самореферентность ненужной) и добавил дополнительный случай: сканирование в ints. Возможность сканировать по разным типам лучше иллюстрирует необходимость наличия аргумента void* как в структуре функтора, так и в указателе функции sig. Вот новый код:

#include <stdio.h>

typedef int (func_t)(int offset, const char * src, void * extra);
typedef struct { func_t * func; void * data; } ftor_t;
typedef struct { int * count; double * dest; } extra_dbl_t;
typedef struct { int * count; int * dest; } extra_int_t;

void process_string(char * buf, int skip, func_t ** func) {
    ftor_t * ftor = (ftor_t*)func;  // <---- strict-alias violation? or UB?
    for (int i = skip; i < 8; ++i) {
        (void)ftor->func(i, buf+i*5, ftor->data);
    }
}

int scan_in_double(int offset, const char * src, void * extra) {
    extra_dbl_t * in = extra;
    return *in->count += sscanf(src, "%lg", in->dest + offset);
}

int scan_in_int(int offset, const char * src, void * extra) {
    extra_int_t * in = extra;
    return *in->count += sscanf(src, "%d", in->dest + offset);
}

int print_repeated(int offset, const char * src, void * extra) {
    // extra not used
    char tmp[10] = {0};
    sscanf(src, "%s", tmp);
    printf("%s %s\n", tmp, tmp);
    return 0;
}

int main()
{
    // contrived strings to make the simplistic +5 in process_string work
    // (the real process_string would use whitespace to non-whitespace
    // transition)
    char dbl_line[50] = "12.3 34.5 56.7 78.9 32.1 65.4 87.4 65.0";
    char int_line[50] = "1234 3456 5678 7890 3210 6543 8743 6501";

    int n_ints_read = 0;
    int int_vals[8] = {0};

    extra_int_t int_data = { .count=&n_ints_read, .dest=int_vals };
    ftor_t ftor0 = { scan_in_int, &int_data };
    process_string(int_line, 0, &ftor0.func);

    // check that it worked properly
    printf("n_ints_read: %d\n", n_ints_read);
    for (int i = 0; i < 8; ++i) {
        printf("int_vals[%d]: %d\n", i, int_vals[i]);
    }
    
    int n_dbls_read = 0;
    double dbl_vals[8] = {0};

    extra_dbl_t dbl_data = { .count=&n_dbls_read, .dest=dbl_vals };
    ftor_t ftor1 = { scan_in_double, &dbl_data };
    process_string(dbl_line, 3, &ftor1.func);

    // check that it worked properly
    printf("n_dbls_read: %d\n", n_dbls_read);
    for (int i = 0; i < 8; ++i) {
        printf("dbl_vals[%d]: %f\n", i, dbl_vals[i]);
    }
    
    ftor_t ftor2 = { print_repeated };  // no extra data req'd
    process_string(dbl_line, 0, &ftor2.func);

    return 0;
}

Но если вместо этого я приму ptr для структуры/функтора:

void process_string(char * buf, int skip, ftor_t * ftor) {
    for (int i = skip; i < 8; ++i) {
        (void)ftor->func(i, buf+i*5, ftor->data);
    }
}

И измените сайт вызова на:

process_string(dbl_line, 0, &ftor2);  // not &ftor2.func

Тогда в process_string() нет приведения указателя и, следовательно, нет нарушения строгого псевдонима. Я думаю.

В обоих случаях новый вывод:

n_ints_read: 8
int_vals[0]: 1234
int_vals[1]: 3456
int_vals[2]: 5678
int_vals[3]: 7890
int_vals[4]: 3210
int_vals[5]: 6543
int_vals[6]: 8743
int_vals[7]: 6501
n_dbls_read: 5
dbl_vals[0]: 0.000000
dbl_vals[1]: 0.000000
dbl_vals[2]: 0.000000
dbl_vals[3]: 78.900000
dbl_vals[4]: 32.100000
dbl_vals[5]: 65.400000
dbl_vals[6]: 87.400000
dbl_vals[7]: 65.000000
12.3 12.3
34.5 34.5
56.7 56.7
78.9 78.9
32.1 32.1
65.4 65.4
87.4 87.4
65.0 65.0

person textral    schedule 08.08.2020    source источник
comment
@chux: я забыл добавить return 0;   -  person textral    schedule 08.08.2020
comment
кроме придирчивой опечатки (возвращаемое значение не используется, поэтому нет UB) я не вижу никаких проблем в вашем коде. Я не знаю, для чего нужны указатели функций в этом примере, но доступ к ним осуществляется только через указатель char.   -  person 0___________    schedule 08.08.2020
comment
Для меня это означает, что уровень предупреждения, например '-Wpedantic' '-Wall' '-Wextra' '-Wconversion', не использовался. Удаление отвлекающих предупреждений вносит ясность в пост.   -  person chux - Reinstate Monica    schedule 08.08.2020
comment
Я полностью согласен с @chux: использование дополнительных флагов приводит только к нескольким отсутствующим инициализаторам для поля «_n» предупреждений «functor_t» (где n равно 3 для инициализации ftor1 и 1 для инициализации ftor2)   -  person textral    schedule 08.08.2020
comment
Идентификаторы, начинающиеся с _, зарезервированы только для определенных целей. Вы не должны использовать такие идентификаторы.   -  person RobertS supports Monica Cellio    schedule 08.08.2020
comment
Почему вы думаете, что это не закрытие? Он несет указатели по значению   -  person Ajay Brahmakshatriya    schedule 08.08.2020
comment
@AjayBrahmakshatriya: потому что именно объекты, на которые указывают, а не сами указатели, имеют значение для функции (указателя) в 1-м элементе структуры   -  person textral    schedule 08.08.2020
comment
@textral Я вижу, что функция на самом деле заботится об указателе (а затем обращает на него внимание, как и на любой другой указатель). Таким образом, это все еще замыкание с разными типами для захваченных значений. Думаю, сейчас мы бы просто спорили о номенклатуре. .   -  person Ajay Brahmakshatriya    schedule 08.08.2020
comment
Что сказал @Ajay: это закрытие - вы просто передаете некоторые аргументы указателя в функцию, а не передаете их напрямую.   -  person psmears    schedule 08.08.2020
comment
@psmears & AjayBrahmakshatriya: не будут ли замыкания работать должным образом при вызове после того, как их аргументы, на которые они ссылаются, выходят за рамки? Моя вещь рухнет в этом случае. (Но вы заставили меня усомниться в моей прежней уверенности, поэтому я отредактировал вопрос, чтобы отразить это.)   -  person textral    schedule 08.08.2020


Ответы (3)


  1. Как называется эта техника?

Обфускация.

Он имеет сходство с замыканиями и с каррирование аргументов, но я бы не назвал это ни тем, ни другим.

Он также имеет сходство со структурой и практикой объектно-ориентированных программ, но упор на преднамеренном сокрытии типов аргументов не занимает особого места в этом режиме.

Также есть подсказка о функции обратного вызова.

В целом, однако, это просто чрезмерно абстрактный беспорядок.

Было полезно передавать разрозненные подпрограммы другой для вызова, потому что вызывающей подпрограмме не нужно знать точный состав аргументов переданных подпрограмм.

Я думаю, ты обманываешь себя.

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

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

  1. Нарушает ли код строгое правило псевдонимов?

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

  1. Вызывает ли код Undefined Bahavior?

Не по своей сути, но да в случае нарушения строгого сглаживания.

person John Bollinger    schedule 08.08.2020
comment
Ах да, каррирование аргументов, я об этом не подумал, но да, вы правы. - person textral; 09.08.2020
comment
один и тот же пользователь упаковывает как вызываемую функцию, так и аргументы для ее передачи -- да, именно так я его и использую, в основном, чтобы высушить свой код. Есть ли лучший способ написать процедуру диспетчеризации, которая может выполнять любую заданную функцию и ее список аргументов, при этом некоторые аргументы заполняются диспетчером таким образом, чтобы сохранить статическую проверку типов? (Может быть, это должен быть новый вопрос SO сам по себе?) - person textral; 09.08.2020
comment
Нет, @textral, то, что я описал, не, как вы его используете, по крайней мере, не в примерах, представленных в вопросе. Ваша основная функция присваивает функцию функтору и передает ее process_string, который присваивает ей аргументы. Я понимаю, что вы в восторге от этого изобретения, но, честно говоря, оно извращенное. Я ценю стремление к СУХОСТИ, но если это мотивация, то код доходит до неразумных, контрпродуктивных крайностей. - person John Bollinger; 09.08.2020
comment
Есть ли лучший способ написать процедуру отправки, которая может выполнять любую заданную функцию и ее список аргументов [...]? В C нет хорошего способа сделать это, включая тот, который представлен в вопросе. И я считаю, что это не имеет значения, потому что проблема, которую вы предлагаете, является слишком общей. Интерфейсы обратного вызова C, которые позволяют провайдеру обратного вызова передавать некоторые аргументы, обычно определяют конкретный прототип для функции обратного вызова и допускают один предоставленный пользователем аргумент типа void *. Это достаточно общий подход, поскольку данные, предоставляемые пользователем, могут быть структурой с произвольными элементами. - person John Bollinger; 09.08.2020
comment
Дизайн карри Лорана Дами (citeseerx.ist.psu .edu/viewdoc/) имеет лучшую инкапсуляцию для передачи внешним пользователям/коду. Я все еще читаю его, но он не выглядит нестандартным (используя расширение вложенной функции gcc). Но он страдает тем же отсутствием проверки типов, что и мой. Как бы вы оценили механизм Дами, @JohnBollinger? - person textral; 09.08.2020
comment
@textral, я бы согласился с Дами в том, что ограничения этого механизма - высокая стоимость времени выполнения, отсутствие проверки типов - делают его пригодным только для экономного использования. Более того, любая реализация этих базовых идей, построенная поверх существующего языка, определенно нестандартна, потому что зависит от преобразования указателя объекта в указатель на функцию, что C не поддерживает. - person John Bollinger; 09.08.2020
comment
Что касается вложенных функций GCC, (i) они нестандартны (представляя собой расширение), поэтому они не могут предоставить средства для реализации идей каррирования функций Dami в соответствии со стандартом; (ii) вложенные функции GCC могут обращаться к переменным основной функции только до тех пор, пока основная функция не выйдет, что недостаточно для общего каррирования функций; и (iii) в той мере, в какой они применимы и вы готовы на них положиться, вложенные функции GCC во всех отношениях лучше, чем предложение Дами, поэтому нет смысла использовать вложенные функции для реализации идей Дами. - person John Bollinger; 09.08.2020
comment
@JohnBollinger: Еще одна проблема заключается в том, что семантика вложенных функций gcc недостижима во всех средах выполнения, и зависимость от них может привести к созданию кода, который нельзя заставить хорошо работать в некоторых средах без существенной доработки. - person supercat; 10.08.2020
comment
Хорошее наблюдение, @supercat, согласен. - person John Bollinger; 10.08.2020
comment
@JohnBollinger: Реализации, чье адресное пространство кода больше, чем адресное пространство данных, могут обеспечивать двусторонние преобразования, имея таблицу в пространстве данных машинных адресов всех функций, чей адрес взят, и наличие указателей функций, содержащих смещения внутри этого стол. Мне кажется несколько любопытным, что такие конструкции используются не так часто, особенно в компиляторах для 8-битных платформ, поскольку даже программы, занимающие 128 КБ кода, редко содержат более 63 функций любого конкретного типа чей адрес взят, и вызов таких функций через таблицу... - person supercat; 10.08.2020
comment
... будет быстрее, чем пытаться загрузить все три байта 24-битного указателя. - person supercat; 10.08.2020
comment
@supercat, когда я говорю, что указатели объектов не могут быть преобразованы в указатели функций, я имею в виду это в том смысле, в котором Стандарт использует термин преобразование. Это уместно здесь, потому что реализация каррирования Dami включает в себя динамическое построение реализаций функций в памяти (что можно сделать только с помощью указателя объекта), а затем предоставление указателя функции, указывающего на эту память, что требует преобразования указателя объекта в указатель функции. Я не понимаю, как описанный вами табличный подход может быть применен к этому сценарию. - person John Bollinger; 11.08.2020
comment
@JohnBollinger: возможность выполнения кода из доступного для записи хранилища — это отдельная проблема. Обычные ограничения стандарта на преобразование проистекают из возможности того, что пространство кода и данных может иметь разный размер. Я предпочитаю передавать методы с помощью двойного косвенного указателя, который может автоматически поддерживаться реализациями, хотя указатели на методы будут несовместимы с обычными указателями на функции. - person supercat; 11.08.2020
comment
@supercat, вопрос о возможности выполнения кода из доступного для записи хранилища не является моей точкой зрения. Стандарт определяет преобразования типов, которые могут быть выполнены. Указатели объектов могут быть преобразованы в другие указатели объектов, но не существует определенного преобразования указателя объекта в указатель функции. Можно выполнить косвенное преобразование через промежуточный целочисленный тип, при условии, что доступен подходящий целочисленный тип, но стандарт ничего не говорит о том, что может произойти, если кто-то попытается выполнить вызов функции через эту функцию. - person John Bollinger; 11.08.2020
comment
Короче говоря, стандарт C не позволяет программам динамически создавать функции. - person John Bollinger; 11.08.2020
comment
@JohnBollinger: если бы в C были средства обозначения типов структур, для которых использование синтаксиса вызова функции для указателя на такую ​​структуру должно обрабатываться как вызов функции через первый член структуры с адресом структуры, передаваемым в качестве аргумента перед всеми остальными, тогда можно было бы использовать код, использующий указатели на такие структуры способом, синтаксически совместимым с использованием обычных функций, и было бы возможным для переносимого кода создавать указатели методов, которые могли бы инкапсулировать произвольные данные. - person supercat; 11.08.2020

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

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

Вот демонстрация того, что я имею в виду:

#include <stdint.h>
#include <string.h>
#include <stdio.h>
typedef void (*streamOutFunc)(void *, void const *dat, uint32_t len);
struct StringStream
{
    streamOutFunc func;
    char *dest;
    uint32_t size,len,totlen;
};
void putStringStreamFunc(void *param, void const *dat, uint32_t len)
{
    struct StringStream *it = param;
    uint32_t maxLen = it->size - it->len;
    uint32_t newTot = it->totlen + len;
    if (newTot < len)
        newTot = -1;
    if (len > maxLen)
        len = maxLen;
    memcpy(it->dest+it->len, dat, len);
    it->totlen = newTot;
    it->len += len;

}
struct FileStream
{
    streamOutFunc func;
    FILE *f;
};
void putFileStreamFunc(void *param, void const *dat, uint32_t len)
{
    struct FileStream *it = param;
    fwrite(dat, len, 1, it->f);
}
void outputSomething(streamOutFunc *stream, void const *dat, uint32_t len)
{
    (*stream)(stream, "Message: [", (sizeof "Message: [")-1);
    (*stream)(stream, dat, len);
    (*stream)(stream, "]\n", (sizeof "]\n")-1);
}
int main(void)
{
    char msgBuff[20];
    struct StringStream myStringStream =
      {putStringStreamFunc, msgBuff, sizeof msgBuff, 0, 0};
    
    outputSomething(&myStringStream.func, "TESTING 12345", (sizeof "TESTING 12345")-1);

    struct FileStream myFileStream =
      {putFileStreamFunc, stdout};
    outputSomething(&myFileStream.func, msgBuff, myStringStream.len);

}
person supercat    schedule 09.08.2020
comment
Спасибо @supercat: если я просто передам двойной косвенный указатель на функцию, мне придется привести его к указателю на структуру, чтобы передать его в качестве аргумента функции ... не нарушит ли это правило строгого псевдонима? (Это тесно связано с другим моим вопросом: stackoverflow.com/q/63175051/1206102) - person textral; 10.08.2020
comment
@textral: функция должна принимать void* и сама выполнять приведение, поскольку она будет знать фактический тип ожидаемого объекта. Если processWoozle() ожидает указатель на WOOZLE_PARAMS, код построения параметров, не использующий тип WOOZLE_PARAMS, не должен хранить в них адрес processWoozle(). - person supercat; 10.08.2020
comment
Я переработал свой пример, чтобы передать двойной косвенный указатель на функцию, см. мое редактирование в нижней части моего исходного сообщения. Возможно, я не совсем понял, что вы имели в виду, поскольку я не мог избавиться от функции-ptr-cast-to-object-ptr, поэтому я все еще могу нарушать строгое сглаживание или даже на территорию UB. Тем не менее, это работает и выглядит намного лучше, чем было! Но см. дальнейший комментарий ниже полного 2-го примера: если вместо этого я передам функтору ptr, я полностью избавлюсь от приведения и избегаю проблем со строгим псевдонимом. Я думаю, это то, что вы имели в виду под своим вторым абзацем в своем ответе, верно? - person textral; 11.08.2020
comment
@textral: посмотри мою демонстрацию. - person supercat; 11.08.2020
comment
Ах я вижу! Вы приводите указатель функции к указателю объекта через посредника void * (1-й аргумент функции) без явного приведения. Хороший! Я думаю, @JohnBollinger будет утверждать, что это UB? Или определяется реализацией? - person textral; 12.08.2020
comment
@textral: в void* преобразуется не указатель функции, а адрес объекта, в котором хранится указатель функции. - person supercat; 12.08.2020
comment
Ух ты! Хорошо, правильно: адрес func ptr эквивалентен адресу объекта, содержащего его, в силу того факта, что первый является 1-м элементом последнего (я думаю, это явно указано в стандарт). Это блестяще. Делая func ptr первым элементом структуры, вы заставляете компилятор обрабатывать адрес func ptr так же, как адрес объекта, включая разрешение кругового преобразования через void*-промежуточное правило. - person textral; 12.08.2020
comment
Кстати, я принял ответ Джона, потому что он хорошо ответил на все вопросы, которые я поставил, особенно на первый вопрос об определении используемых методов. Ваш был более полезен для улучшения моего решения, но, к сожалению, я не задал этот вопрос в исходном сообщении. - person textral; 12.08.2020

Определение функтора см. на странице https://en.wikipedia.org/wiki/Functor. Здесь это не кажется уместным.

По сути, именно так вы можете реализовать объектно-ориентированное программирование на C.

Вы видите эту технику в ядре Linux для описания драйверов устройств. Дескриптор драйвера содержит указатели на функции и некоторые дополнительные данные, например:

    static struct platform_driver meson_rng_driver = { 
        .probe  = meson_rng_probe, // a function
        .driver = {
                .name = "meson-rng",
                .of_match_table = meson_rng_of_match,
        },
    };

Linux собирает эти дескрипторы драйверов в списки, сгенерированные компоновщиком.

В объектно-ориентированном программировании определение структуры (здесь struct platform_driver) представляет собой интерфейс, а структура с фактическими указателями функций — на класс, а функции указывают на методы класса. Поля данных содержат переменные уровня класса.

Нет никакого неопределенного поведения. Нет нарушения строгого алиасинга.

person Xypron    schedule 08.08.2020
comment
Этот ответ не касается ни одного из трех заданных вопросов - person Ajay Brahmakshatriya; 08.08.2020
comment
Он отвечает, как вызвать эту технику. - person Xypron; 08.08.2020