Pull to refresh

Comments 35

UFO landed and left these words here

Когда это сишечка стала языком со строгой типизацией? Тут слабая статическая типизация же.

Да, как говорится, писал об одном, думал о другом. Попутал с C++. Поэтому я сразу был и удалил тот комментарий, на который вы ответили
Оффтоп! Странно, почем хабр допустил такое. Вы изменили свой комментарий, а пользователь смог прокомментировать еще не изменённый! Вроде нельзя изменять комментарий на который ответили или прокомментировали? Это ведь должно работать и «здесь и сейчас»? По хорошему, комментаторам «должно придти» сообщение, что нельзя менять своё или комментарий изменён (посмотрите).
Не знаю как такое прошло, но когда я изменил свой комментарий, ответов на него еще не было. Ответ пришел гораздо позже.
Вы совершенное правы, в личку мне тоже об этом писали, к сожалению, не было времени вчера еще исправить, но уже поправил. Статья старая, хоть и перечитывал, не обратил на эту досадную ошибку внимания. Имелась ввиду статическая типизация, конечно же.
В GLib примерно так реализовано ООП.
	int b = (int)a;

	return b == (int)(a - 0.5) // если дробная часть >= 0.5
			? b + 1 // округляем в плюс
			: b; // отбрасываем дробную часть

  1. К чему такие сложности? Это всё можно заменить на эквивалентное
    return (int)(a + 0.5);
  2. Это ненастоящее округление, оно неправильно работает с отрицательными числами.
Совершенно верное замечаение, хоть и не имеет прямого отношения к теме, все же не стоит транслировать плохие решения в массы, так сказать. Внес исправления, спасибо.
Я могу ошибаться, и пусть language lawyers меня исправят если так, но как только ты делаешь
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* такого не требуется.

union гарантирует, что поля буду располагаться по одному адресу в памяти, в том числе и без необходимости отключать выравнивание, без которого все равно могут быть неожиданные моменты.
UFO landed and left these words here
Это в C++. В C такого ограничения нет, и делать type punning через union вполне допустимо.
UFO landed and left these words here
Продублирую здесь тоже.

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 на эту тему.
UFO landed and left these words here

Всё же unspecified это не то же undefined. Первое это "мы не обещаем что получится", а второе "у вас всё поломалось и не будет работать больше никогдаа". То есть так делать можно но стандарт не обещает вам что получится то, что вы хотите.

И что касается C++, ни в коем случае так не делайте, там для этого есть std::variant или аналоги.
… которые примерно так и реализованы.
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);
UFO landed and left these words here

А кто конвертит-то? Мы проверяем type, если сохраняли type 0, то и поле в union сохранили соответствующее, и читать будем его же. Т.е. как раз поле type хранит информацию о том, какое поле в union было сохранено, чтобы читать его же и избежать UB.

UFO landed and left these words here

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


Округление переписал с использованием 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.
Речь шла не об арифметике указателей, а об арфиметике с типом void, которое, впрочем, также запрещено, исправил эту часть, спасибо за замечание.
В языке C используется строгая типизация, что, на мой взгляд более, чем правильно.

Не "строгая", а слабая статическая.

Sign up to leave a comment.

Articles