Comments 35
Когда это сишечка стала языком со строгой типизацией? Тут слабая статическая типизация же.
int b = (int)a;
return b == (int)(a - 0.5) // если дробная часть >= 0.5
? b + 1 // округляем в плюс
: b; // отбрасываем дробную часть
- К чему такие сложности? Это всё можно заменить на эквивалентное
return (int)(a + 0.5);
- Это ненастоящее округление, оно неправильно работает с отрицательными числами.
dStruct j;
...
lilround(&j);
а потом сразу же в функцииiStruct *s = (iStruct *)arg;
if(s->type == 0) // если передан int
то ты получаешь UB и самолёт падает в болото. Потому что из void* можно читать только тот тип, который туда записали.Поэтому такую прости господи «динамическую типизацию» делают через union:
struct numeric {
union {
int integer;
double fractional;
}
char type;
}
Вроде как, наоборот, из union можно читать только то, что записали.
Про void* такого не требуется.
that do not correspond to that member but do correspond to other members
Это не про то. Это про случай типа такого:
union U {
int i;
char c;
};
Если мы здесь что-то пишем в c, то та часть i, которая не пересекается с c, принимает unspecified значение. Type punning через union и в C99, и в последующих стандартах C (но не C++) прямо и явно разрешен:
www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf, параграф 6.5.2.3, сноска 82:
If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called «type punning»).
www.open-std.org/jtc1/sc22/wg14/www/docs/dr_257.htm — уточняющий (и принятый) defect report на эту тему.
int *i_ptr = (int *)(var);
double *d_ptr = (double *)(var);
Эти приведения типов не нужны в C. Там любой указатель приводится к void*
, и void*
приводится к любому указателю.
int *i_ptr = var;
double *d_ptr = var;
Примечание: в некоторых источниках говорится о том, что присвоение указателю типа void * следует производить также с приведением типа.
Какие источники? Есть один правдивый источник — стандарт C. Всё остальное — частное мнение, не всегда правильное.
#pragma pack(push, 1)
typedef struct {
char type; // идентификатор типа структуры
int value; // целочисленное значение
} iStruct;
#pragma pack(pop)
#pragma pack(push, 1)
typedef struct {
char type; // идентификатор типа структуры
double value; // значение двойной точности
} dStruct;
#pragma pack(pop)
Очень не удачный пример. Для этого существует union
:
#pragma pack(push, 1)
typedef struct {
char type; // идентификатор типа структуры
union {
int intValue; // целочисленное значение
double doubleValue; // значение двойной точности
}
} typedStruct;
#pragma pack(pop)
Хорошим примером вместе с lilround
был бы пример с qsort
.
Округление в одну строчку и без ошибки, так как 0.5 в двоичном виде равно без погрешности 0.1.
double a = *((double *)arg);
int b = (int)(a + 0.5);
Пример с примитивными типами, очевидно, рассмотрен для простоты. Ниже уже идет чуть более осмысленный пример. Объединения хороши для своих задач и требуют сразу указать все возможные типы, а с "интерфесами" можно добавлять реализации по мере необходимости, лишь бы они соответствовали контрактам интерфейса.
Округление переписал с использованием math.h
.
Объединения хороши для своих задач и требуют сразу указать все возможные типы, а с "интерфесами" можно добавлять реализации по мере необходимости, лишь бы они соответствовали контрактам интерфейса.
Что за фигню вы сейчас сказали. Добавить новый тип в union, точно так же, как и отдельный struct - по сложности одна и та же задача, вместо привидения void*
к новому типу нужно будет взять новое поле union. А теперь самое худшее в вашем решении со структурами: привести и разыменовать void*
можно только в тот тип, на который он указывает, иначе это - неопределенное проведение по стандарту.
Разве я что-то говорил про сложность? Добавлять реализаци по мере необходимости означает то, что эти реализации могут добавляться вообще в другом месте. Например, у нас может быть библиотека, работающая с определенным интерфейсом, его реализацию мы можем добавить в своей программе. Как подобная задача будет решаться с использованием объединений скажите, пожалуйста? Сразу отмечу, что вопрос академического характера, то есть не надо рассматривать в контексте "нужно ли оно вообще".
Насчет UB — надо будет пошерстить стандарт.
Да добавляйте реализацию где угодно, причём здесь описание интерфейса?
Не стану делать вид, что понимаю — о чем вы говорите. Вот у нас есть некий условно обобщенный, тип, допустим, он сделан через union, как вы и сказали, и зашит где-то в библиотеке. Если мне нужно добавить поддержку еще одного типа, то что предлагаете делать в таком случае? В случае со структурами — просто описываете новую структуру.
зашит где-то в библиотеке
Что значит зашит? Это не .net library, у вас есть всегда .h и вы вольны его модифицировать.
В совсем тяжелом случае можно сделать так:
typedef struct {
char type;
union {
int intValue;
double doubleValue;
};
} LibraryStructThatCantBeModified;
typedef struct {
union {
LibraryStructThatCantBeModified libraryStruct;
struct {
char type;
union {
int intValue;
double doubleValue;
short shortValue;
};
};
};
} FuckYouICan;
typedef struct {
LibraryStructThatCantBeModified libraryStruct;
short shortValue;
} FuckYouICanAlso;
Если продолжить ваше упрямство, и допустим, эта загадочная библиотека имеет 255 типов, куда вы запихнёте 256 с вашим подходом?
Вообще, всё это выдумано и ни одной такой библиотеки не существует. Посмотрите хотя бы реальные примеры статической типизации в BSD socket API. Ваши примеры становятся расширяемыми без вопросов:
typedef struct {
char type; // идентификатор типа структуры
} Type;
typedef struct {
Type type;
int value; // целочисленное значение
} iStruct;
typedef struct {
Type type;
double value; // значение двойной точности
} dStruct;
И функции работают с базовым типом Type*
, а не void*
. Тогда нет никакого UB.
Реальные библиотеки практически всегда используют динамическую диспетчеризацию.
typedef struct {
void (*func)(void*);
} VTable;
typedef struct
VTable vtable;
} BaseStruct;
typedef struct
BaseStruct base;
int myData;
} MyStruct;
Рекомендую изучить это и написать новую статью.
Если бы вы внимательно прочитали первые пару абзацев и последний, то поняли бы, что цель состоит не в том, чтобы показать, как правильно делать, а в том, чтобы заинтересовать читателя самостоятельно разбираться в вопросе дальше. Отсюда и "упрямство". Это как с изучением какого-то раздела математики, часто можно применять в многих задачах, но, зачастую, есть более оптимальные методы. Я же не призываю делать всегда и именно так, как раз наоборот.
Думаю, вы согласитесь, что последний пример с union ужасен, хотя все зависит от конкретных условий, конечно.
UB возникает, насколько мне удалось понять, только в том случае, если приводятся not aligned типы, так что в данном случае его не будет. Что же касается вашего примера с VTable, то это хороший пример, но он, как вы верно заметили, требует отдельной статьи на которую планов пока нет, ибо этому материалу десяток лет, и на C я давненько не писал, а лишь стряхнул пыль со старого материала. Тем не менее, теперь задумался над вашим предложением.
Перед тем, как перейти к последней части, стоить пояснить работу с простыми void-указателями. Сложение, вычитание, инкремент, декремент и т.д. не запрещены для типа void, однако могут вызывать предупреждения в C++ и не вполне понятное поведение.
Когда это в C и C++ разрешили арифметику с
void *
? Это неопределенное поведение. Читайте ISO/IEC 9899:2011, раздел „6.5.6 Additive operators”, §2.В языке C используется строгая типизация, что, на мой взгляд более, чем правильно.
Не "строгая", а слабая статическая.
Динамическая типизация C