Неявные преобразования типа char

b707
Offline
Зарегистрирован: 26.05.2017

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

Ардуино ИДЕ 1.8.3, плата Уно

Код

char m[] = "Привет";
void setup() {
Serial.begin(115200);
while (! Serial);
for (byte i =0; m[i]; i++ ) Serial.println(m[i], HEX);
}

void loop() {}

Вывод:

FFFFFFD0
FFFFFF9F
FFFFFFD1
FFFFFF80
FFFFFFD0
FFFFFFB8
FFFFFFD0
FFFFFFB2
.....

Если в первой строке кода поменять тип строки на unsigned cahr - печается все нормально, 8-битные значения.

Возникает два вопроса:

- в шестнадцатиричном представлении знаковый и беззнаковый символ совпадают же - так откуда разница?

- судя по количеству символов в выводе, знаковый символ при печати превратился в лонг??? -

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Особенность реализации.

Дело в том, что у класса Print есть метод для беззнакового char и системы счисления

size_t print(unsigned char, int = DEC);

Потому беззнаковый и работает нормально.

А такого же метода для знакового char - нет (т.е. нет метода size_t print(char, int = DEC); )

Компилятор начинает искать походящий метод. Находит для int, но он на самом деле просто прокси на long, смотрите:

size_t Print::print(int n, int base)
{
  return print((long) n, base);
}

Вот Вы и получаете печать long в результате.

b707
Offline
Зарегистрирован: 26.05.2017

Евгений, дело не только в классе принт

Смотрите, если код поменять вот так:

char m[] = "Привет";
void setup() {
Serial.begin(115200);
while (! Serial);
for (byte i =0; m[i]; i++ )  {
 if (m[i] == 0xD0) Serial.println(m[i], HEX);
}
}

void loop() {}

то для unsigned char значения символов 0xD0 будут напечатаны, а для знакового - нет, так как на самом деле компилятор при сравнении в строке 6 тоже конвертирует символы в 0xFFFFFFD0, а это конечно не равно 0xD0

Green
Offline
Зарегистрирован: 01.10.2015

m[i] преобразуется в int и будет равно FFD0, а второй операнд 0xD0 уже int, поэтому и результат false.

DetSimen
DetSimen аватар
Онлайн
Зарегистрирован: 25.01.2017

Вопщем, век живи, - век в канпилятор плюйся. 

sadman41
Offline
Зарегистрирован: 19.10.2016

То есть самый безопасный способ сравнения char - сравнение с другим char?

if (m[i] == '\xD0') Serial.println(m[i], HEX);

 

Green
Offline
Зарегистрирован: 01.10.2015

Или приведением. == (char)0xd0.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:

Евгений, дело не только в классе принт

то для unsigned char значения символов 0xD0 будут напечатаны, а для знакового - нет, так как на самом деле компилятор при сравнении в строке 6 тоже конвертирует символы в 0xFFFFFFD0, а это конечно не равно 0xD0

Это уже другой вопрос (там было "только").  А здесь Green уже ответил. Вы сравниваете char с int, разумеется, перед сравнением всё будет преобразовано к int.

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

sadman41 пишет:

То есть самый безопасный способ сравнения char - сравнение с другим char?

Да, самый безопасный - не смешивать типы в выражении и не полагаться на неявные преобразования.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Коллеги, я тут писал симпатичный (на мой взгляд) примерчик на использование переполнения, и по ходу написания наткнулся на ещё одну красивую ошибку связанную с неявнями преобразованиями типов.

Посмотрите на вот этот код.

void setup(void) {
	Serial.begin(57600);
	
	const uint8_t s = 32, e = 31;
	const uint8_t n = (e - s) % 32;
	const uint8_t m = static_cast<uint8_t>(e - s) % 32;

	Serial.print("n=");  Serial.print(n);
	Serial.print("; m=");  Serial.print(m);
}

void loop(void) {}

///////////////////
//  РЕЗУЛЬТАТ
//
//  n=255; m=31

Все числа до единого описаны как uint8_t, то бишь беззнаковые. Как Вам нравится остаток от деления на 32 равный 255.

Это, собственно, про тоже, о чём тут речь шла. Пояснять, надеюсь, не нужно?

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

 Пояснять, надеюсь, не нужно?

нужно.

Не понимаю, почему выражение в скобках (e-s) в котором оба операнда имеют тип uint8_t, не приводится автоматически к этому же типу? я всегда считал, что если все операнды в выражении одного типа, то результат будет этого же типа без всяких приведений.

Green
Offline
Зарегистрирован: 01.10.2015

int - int = int, int % int = int, а после int приведится к uint8 (пока нет ЕвгенияП)).

b707
Offline
Зарегистрирован: 26.05.2017

Green пишет:

int - int = int, int % int = int, а после int приведится к uint8 (пока нет ЕвгенияП)).

да откуда ж там int - int = int ? там же явным образом ( uint8 - uint8)

Green
Offline
Зарегистрирован: 01.10.2015

Такие правила.( Любые операнды типа char, unsigned char или short преобразуются к типу int.
Подставляем: 31 - 32 = -1, -1 % 32 = -1, преобразуем в uint8 = 255. Сейчас Евгений разнесёт.)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:
почему выражение в скобках ...

Помните эпичный срач про сдвиги, когда Великий газировал лужу насчёт кривизны компилятора, а когда ему показали, что поведение компилятора полностью соответствует стандарту языка, он продолжил газирование, заявлением, что «в стандарте тоже много несуразностей», т.е. «язык тоже кривой» :)

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

Давайте посмотрим на определения операции умножения и деления (*, /, %), а также сложения и вычитания (только то, что нас интересует):

  1. про сложение и вычитание, читаем в разделе 8.7 стандарта: «The usual arithmetic conversions are performed for operands …» (8.7(1), стр. 131)
  2. про (*, /, %) в разделе 8.6. В частности там читаем: «The usual arithmetic conversions are performed on the operands …» (8.6(2), стр. 131).

Ага, значит перед выполнением арифметической операции должны быть применены «usual arithmetic conversions». Смотрим, что за зверь такой. Полное определение «usual arithmetic conversions» даётся в разделе 8(11) стандарта (стр. 94). Там много всего, нас интересует только фраза из п. 8(11.5) «Otherwise, the integral promotions shall be performed on both operands».

Час от часу не легче, значит, для обоих операндов должны быть выполнены «integral promotions»

Этому зверю целиком посвящён раздел 7.6 стандарта. Там тоже хватает всего, но нас интересует пункт 7.6(1) на стр. 89 – «A prvalue of an integer type other than bool, char16_t, char32_t, or wchar_t whose integer conversion rank is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int»

Ну, вот и чудненько. Значит, перед выполнением арифметической операции, мы должны с каждым операндом, размер которого меньше размера типа int, проделать такую штуку:

  1. Если в int помещаются все возможные значения, то преобразовать к типу int;
  2. Иначе, преобразовать к типу unsigned.

Вот и давайте теперь пошагово разберём код:

const uint8_t s = 32, e = 31;
const uint8_t n = (e - s) % 32;

Итак, первым выполняется действие в скобках. Оба операнда короче int. Все их возможные значения (0-255) в int вполне себе помещаются. Значит, e и s будут преобразованы к int, а потом над ними выполнена операция вычитания. Результат очевидно, имеет тим int и равен -1.

Далее выполняется %. Первый операнд уже int, как мы выяснили. Второй - короче int. Все его возможные значения (0-255) в int вполне себе помещаются, стало быть его тоже преобразуют в int. После этого будет выполнена операция % над двумя операндами типа int: "-1 % 32". Результат этой операции, очевидно равен -1 (типа int).

Ну, и операция присваивания. Перед её выполнением, правая часть будет преобразована к типу левой (к uint8_t ), т.е. -1 превратится в 255, что мы ошалело и наблюдаем.

Случай

m = static_cast<uint8_t>(e - s) % 32;

я так подробно разбирать не буду. В скобках также получится -1 типа int, но она тут же будет преобразована к uint8_t и станет 255. При выполнении % оба операнда снова будут преобразованы к типу int и будет выполнена операция "255 % 32". Результат 31. При присваивании, 31 преобразуется к типу uint8_t без видимых изменений.

Ну, вот как-то так. Собственно, здесь нет ничего, чего бы уже не объяснил коллега Green, за что ему спасибо, просто я счёл нужным рассказать с прямыми ссылками на "Завет".

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

inspiritus
Offline
Зарегистрирован: 17.12.2012

Гыыы :) Великий !!!! газировал!!!! лужу !!! 

плюсую неистово :)

b707
Offline
Зарегистрирован: 26.05.2017

Спасибо!

Но я все равно еще спрошу. можно?

как же все-таки обехопасить себя от подобных ситуаций. если даже Ваша рекомендация из #8 ("самый безопасный - не смешивать типы в выражении") - в данном случае не сработала. Все типы в вашем примере одинаковые

И еще я не вполне понял, почему такая разница между char и unsigned char - первый преобразуется к int в сравнении (см код в #2), а второй нет, хотя размер обоих меньше int

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:

И еще я не вполне понял, почему такая разница между char и unsigned char - первый преобразуется к int в сравнении (см код в #2), а второй нет, хотя размер обоих меньше int

Там дело не в сравнение, а в передаче параметров. У класса Print есть метод с параметром unsigned char, потому просто используется этот метод и ничего преобразовавыть не надо. А такого же метода с параметром char нет (авторы не написали), потому компилятор начинает искать подходящий метод, для чего выполняет то самое usual arithmetic conversions, о котором я и писал. Вот char в int и превращается.

sadman41
Offline
Зарегистрирован: 19.10.2016

b707 пишет:

Но я все равно еще спрошу. можно?

как же все-таки обехопасить себя от подобных ситуаций. 

У меня другой вопрос возник сразу после прочтения: как, мать ево, вообще что-то работает у любителей, если такие фокусы компилятор по стандарту исполняет? Каждый второй газовый котёл должен взлетать на воздух...

bwn
Offline
Зарегистрирован: 25.08.2014

sadman41 пишет:

 Каждый второй газовый котёл должен взлетать на воздух...

Так, вроде, газовики, решившие свою задачу, больше на форум и не приходили.))))) Этакий, лайт геноцид.))))

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

Там дело не в сравнение, а в передаче параметров. У класса Print есть метод

Евгений, с классом принт был пример в заглавном посте. А я про код в #2, в котором дело не в классе Print

Сокращенный вариант:

char m[] = "\xD0\xD0";

for (byte i =0; m[i]; i++ )  {
 if (m[i] == 0xD0)     .......
}


В коде выше если m[] -массив типа char, равенство никогда не будет истинным, а для unsigned char все работает так, как ожидается. Поясните, почему в одном случае выполняется преобразование в инт, а в другом нет

 

 

Update. Сорри, туплю. В обоих случаях происходит преобразование, но в случае char преобразование идет в int и получается 0xFFD0 и условие ложно.  А в случае unsigned char преобразуем в unsigned int и получаем 0x00D0, поэтому равенство выполняется

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

sadman41 пишет:

b707 пишет:

Но я все равно еще спрошу. можно?

как же все-таки обехопасить себя от подобных ситуаций. 

У меня другой вопрос возник сразу после прочтения: как, мать ево, вообще что-то работает у любителей, если такие фокусы компилятор по стандарту исполняет? Каждый второй газовый котёл должен взлетать на воздух...

пора забить на канпилятор и писать в кодах?

sadman41
Offline
Зарегистрирован: 19.10.2016

bwn пишет:

sadman41 пишет:

 Каждый второй газовый котёл должен взлетать на воздух...

Так, вроде, газовики, решившие свою задачу, больше на форум и не приходили.))))) Этакий, лайт геноцид.))))

Поэтому, наверное, во всех ардуинопримерах int-ы понатыканы даже там, где им вообще быть не нужно.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:
В обоих случаях происходит преобразование, но в случае char преобразование идет в int и получается 0xFFD0 и условие ложно.  А в случае unsigned char преобразуем в unsigned int и получаем 0x00D0, поэтому равенство выполняется
Именно так!

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

ua6em пишет:

пора забить на канпилятор и писать в кодах?

Вы уверены. что качество полученного кода будет лучше, чем у канпилятора? :)))

mykaida
mykaida аватар
Offline
Зарегистрирован: 12.07.2018

ua6em пишет:

пора забить на канпилятор и писать в кодах?

Ага - и искать ошибки в программе с тысячей строк причем не структурированных. Писал - удовольствие ниже среднего.

andycat
andycat аватар
Offline
Зарегистрирован: 07.09.2017

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

Green
Offline
Зарегистрирован: 01.10.2015

Особенно много запаса можно разместить например в Тини13.)
Я отдельным табом держу в редакторе приоритеты и правила преобразований типов. Потому что плохо когда знаешь и забыл (ещё хуже когда не знаешь и забыл)).

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

ЕвгенийП пишет:

ua6em пишет:

пора забить на канпилятор и писать в кодах?

Вы уверены. что качество полученного кода будет лучше, чем у канпилятора? :)))

у Вас то однозначно будет лучше, ну или не хуже )))

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

sadman41 пишет:

У меня другой вопрос возник сразу после прочтения: как, мать ево, вообще что-то работает у любителей, если такие фокусы компилятор по стандарту исполняет? Каждый второй газовый котёл должен взлетать на воздух...

Если память мне не изменяет, далеко не все новички отписываются о результатах по окончании своих экспериментов.

Logik
Offline
Зарегистрирован: 05.08.2014

sadman41 пишет:

У меня другой вопрос возник сразу после прочтения: как, мать ево, вообще что-то работает у любителей, если такие фокусы компилятор по стандарту исполняет?

А ну отставить беспорядок нарушат! А то додик в стрингах разяснит что.

wdrakula пишет:

 Ты "наехал" на компилятор - это всегда принак тупости и непрофессионализма. Ровно так Архат уже делал. 

;)
sadman41
Offline
Зарегистрирован: 19.10.2016

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

Моё удивление связано с тем, что широкие массы любителей, внезапно получивших доступ к лёгким способам разработки и, на третий день после покупки development board, без задней (а часто - и передней) мысли пытающихся собирать контроллеры, управляющие потенциально опасными устройствами, так редко сталкиваются с проблемами неявного приведения типов, что взрывы их газовых котлов не слышны каждые пять минут.

SLKH
Offline
Зарегистрирован: 17.08.2015

sadman41 пишет:

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

Моё удивление связано с тем, что широкие массы любителей, внезапно получивших доступ к лёгким способам разработки и, на третий день после покупки development board, без задней (а часто - и передней) мысли пытающихся собирать контроллеры, управляющие потенциально опасными устройствами, так редко сталкиваются с проблемами неявного приведения типов, что взрывы их газовых котлов не слышны каждые пять минут.

Ничего удивительного нет. Кто взорвался, тот не пишет - так что про взрывы мы не знаем.

"...известно расхожее мнение о доброте дельфинов, основанное на рассказах пловцов, которых животные толкали к берегу, но нет данных от тех, кого толкали в обратном направлении"

С другой стороны, всё не так трагично. Конструкторы контроллеров "на третий день после покупки" просто не доходят до стадии работающего контроллера, т.к. не могут включить китайский релейный модуль, спрашивают про него здесь, обижаются и бросают это дело. Или постепенно доходят - но тогда и контроллер может получиться вполне приличным.

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

А я тут заметил, что вот эти ужастики про signed/unsigned – это ведь прямой ответ на часто всплывающий вопрос: как лучше определять константы – макросами через define или константами через const. Как раз, второй случай даёт нам полный контроль, а с первым – на откуп компилятору.

Green
Offline
Зарегистрирован: 01.10.2015

Да, с define это не всегда явно. Просто нужно знать что по умолчанию это int, но ничто не мешает указывать явно, дописывая U, L или UL, или вообще тип (uint8_t)XX. С другой стороны, тут как то своему пацану делал задание на C# - так там вообще нет препроцессора... Никакой тебе условной трансляции, никаких тебе define. Для меня это была тоска... Просто неудобно.)

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Green пишет:

делал задание на C# - так там вообще нет препроцессора... Никакой тебе условной трансляции, никаких тебе define. Для меня это была тоска... Просто неудобно.)

https://docs.microsoft.com/ru-ru/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-define

Есть всё, и даже ConditionalAttribute. Т.е. сделать можно всё, определив директиву, а константы загнать в статические элементы структуры, которую (структуру) определить по разному в зависимости от какого-то define. Конечно, всё не так, как в С++, но кой-чего сделать можно. Впрочем, обычно не возникает необходимости, поскольку подход к программированию на С# - другой, и там и без препроцессора всё нормально реализуется.

Green
Offline
Зарегистрирован: 01.10.2015

Ну да, с наскоку не разобрался.( Немного по другому, конечно. Просто сразу подумал, ну раз "старший брат" от этого отказался, значит что то в этом есть.)