2015-05-10 2 views
4

Иногда полезно отключать обратные вызовы функций.Предотвращение несовместимых функций в C?

Например, мы можем иметь функцию, чтобы дублировать некоторые данные:
struct MyStruct *my_dupe_fn(const struct MyStruct *s)

Но передать его в качестве родового обратного вызова:
typedef void *(*MyGenericCopyCallback)(void *key);

Например:
ensure_key_in_set(my_set, my_key, (MyGenericCopyCallback)my_dupe_fn);

Поскольку разница между const struct MyStruct * и void * не вызовет проблем в этом случае, это не вызовет ошибок (по крайней мере, при вызове функции)..

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

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



Обязательная оговорка: конечно C не * сейф *, но способы, чтобы предотвратить возможные ошибки в широко используемом языке по-прежнему полезны.

+0

вы можете написать '' my_dupe_fn'' с подписью typedef'ed "contract" и отбросить ваши аргументы внутри функции до требуемого типа. Тогда вам не понадобится бросок, и вы будете «надежны» в отношении этих потенциальных будущих изменений. Хорошим примером для этого является «DWORD ThreadProc (параметр void *)». Я никогда не видел, чтобы кто-то пытался отличить другую типизированную функцию к общей версии. Обычно люди вводят параметр в ожидаемый тип внутри '' ThreadProc''. – BitTickler

+1

Есть несколько способов, чтобы кто-то не делал что-то глупое в C, и это не одно из немногих. Это точка зрения * cast *, чтобы принудительно сказать компилятору «Я нахожусь на этом, не беспокойся на вашей стороне». Любой указатель кода может быть вынужден носить эту маскировку. В конечном счете, это * обязанность звонящего * обеспечить правильный интерфейс. Если они заставляют бросить, * они * сбросили мяч. Они должны писать правильную функцию соответствия и * не * отличать их fptr * точно *, чтобы разрешить эту ситуацию при изменении интерфейса. – WhozCraig

+0

Не уверен, но: действительно ли приведение 'my_dupe_fn' действительно требуется?void * может быть назначен любому другому типу указателя без приведения в действие, но я не уверен, работает ли это с этой косвенностью. – Olaf

ответ

1

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

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

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

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

+1

Хотя это правильный ответ, его не совсем правильно предложить что компилятор никогда не поймает это. Всевозможные действительные C-коды, о которых компиляторы могут (необязательно) предупреждать, потому что они часто являются причиной ошибок. – ideasman42

+1

Очень верно. Существуют определенные способы автоматизировать проверку на практике подобных вещей. Однако я хотел сказать, что C не имеет языковых гарантий для того, чтобы понять что-то подобное - то есть, я не знаю, как написать это, чтобы ошибка на самом деле была недействительной C-кодом (я бы хотел чтобы быть доказанным неправильно на тот момент, хотя, поскольку это было бы очень полезно). –

+0

C не имеет сильной типизации. Большая часть нетривиальной программы должна каким-то образом сворачивать с помощью типов и полагаться на правильный тип. Стандартным примером является malloc: программист должен убедиться, что тип результата соответствует типу принимающего указателя (т. Е. Размер fo malloc и совпадение типа указателя). – Olaf

2

Если вы используете gcc и не боитесь использовать полезные расширения, вы можете взглянуть на plan9-extensions. В сочетании с анонимными полями структуры (стандартными начиная с C99) в качестве первого поля они позволяют создавать иерархию типов со статическими функциями и т. Д. Избегайте тонны отбрасываний в моем коде и делают их более читаемыми.

Не уверен, но в соответствии с документацией gcc, MS-компилятор поддерживает некоторые (все?) Эти функции. Однако никаких гарантий для этого нет.

3

Вы говорите: «не будет вызывать ошибок», однако он вызывает неопределенное поведение для вызова функции с помощью указателя функции с несовместимыми типами возвращаемых данных или типами параметров, даже в вашем примере кода.

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

В вашем примере это может быть:

typedef void *MyCallback(void *key); // style: avoid pointer typedefs 

struct MyStruct *my_dupe_fn(const struct MyStruct *s) 
{ ... } 

void *my_dupe_fn_callback(void *s) 
{ 
    return my_dupe_fn(s); 
} 

void generic_algorithm(MyCallback *callback) 
{ 
    // .... 
    ensure_key_in_set(my_set, my_key, callback); 
    // .... 
} 

// elsewhere 
generic_algorithm(my_dupe_fn_callback); 

Обратите внимание на отсутствие слепков. Управление политикой стиля, не использующей какие-либо функции, проще, чем политика разрешения определенных типов.

+0

Ну, то есть, однако, довольно сложно читать/undestand. Особенно в нетривиальном коде. – Olaf

+0

@ Олаф, что трудно читать или понимать? (Я должен был догадаться о подробностях о примере, так как это было довольно просто) –

+0

Точка в исходном вопросе заключается в том, что функция вызовет свое «я», не вызовет ошибок. Конечно, внутренняя структура аргументов или использование возвращаемого значения - все еще имеет возможность ошибки. – ideasman42

0

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

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

typedef void* GenericFunction(int argc, void** args); 

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

void* MyCallback(int argc, void** args) 
{ 
    assert(argc == 2); 
    ... 
    return 0; 
} 

Если вам нужно больше безопасности, чем это для отдельных аргументов, передаваемых и можете позволить себе типично небольшую стоимость дополнительного указателя на каждом аргумент с немного громоздким структурированным решением, вы можете сделать что-то вроде этого:

struct Variant 
{ 
    void* ptr; 
    const char* type_name; 
}; 

struct Variant to_variant(void* ptr, const char* type_name) 
{ 
    struct Variant new_var; 
    new_var.ptr = ptr; 
    new_var.type_name = type_name; 
    return new_var; 
} 

void* from_variant(struct Variant* var, const char* type_name) 
{ 
    assert(strcmp(var->type_name, type_name) == 0 && "Type mismatch!"); 
    return var->ptr; 
} 

void* pop_variant(struct Variant** args, const char* type_name) 
{ 
    struct Variant* var = *args; 
    assert(var->ptr && "Trying to pop off the end of the argument stack!"); 
    assert(strcmp(var->type_name, type_name) == 0 && "Type mismatch!"); 
    ++*args; 
    return var->ptr; 
} 

С макросами, как так:

#define TO_VARIANT(val, type) to_variant(&val, #type); 
#define FROM_VARIANT(var, type) *(type*)from_variant(&var, #type); 
#define POP_VARIANT(args, type) *(type*)pop_variant(&args, #type); 

typedef struct Variant* GenericFunction(struct Variant* args); 

Пример обратного вызова:

struct Variant* MyCallback(struct Variant* args) 
{ 
    // `args` is null-terminated. 
    int arg1 = POP_VARIANT(args, int); 
    float arg2 = POP_VARIANT(args, float); 
    ... 
    return 0; 
} 

Дополнительным преимуществом является то, что вы можете увидеть в вашем отладчике при трассировке в MyCallback через эти type_name полей.

Это может быть полезно, если ваша кодовая база поддерживает обратные вызовы в динамически типизированные языки сценариев, поскольку языки сценариев не должны делать типы приведения в свой код (как правило, они должны быть немного на более безопасной стороне) , Имена типов затем могут использоваться для автоматического преобразования аргументов в собственные типы языка сценариев динамически с использованием полей type_name.

Смежные вопросы