С++. Неявные преобразования численных типов

Для понимания этой темы необходимо знать:

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

С этим связаны первые вопрошения: "Почему не работает?". Такие особенности языка C++ болезненно сказываются в первое время обучения, потому что при выполнении каких-то математических расчётов иногда мешает.
Для компьютера, в отличие от человека, разные типы представляют из себя некие сущности с разным внутренним строением. Из-за разного внутреннего строения типов компьютеру для обработки данных некоторых разных типов необходимо использовать разные инструкции.

Например, при сложении двух значений, имеющих тип short, могут использоваться аппаратные инструкции, отличные от применяемых при сложении двух значений long. При наличии 11 целочисленных типов и 3 типов чисел с плавающей точкой компьютер сталкивается со множеством случаев их обработки. Чтобы справиться с потенциальной неразберихой в типах, многие преобразования типов в C++ осуществляются автоматически.

  • Автоматические преобразования типов называют неявным приведением типов
Преобразование типов возможно в двух вариантах:
1. Узкий тип преобразовывается к широкому типу. Уширение типа.
2. Широкий тип преобразовывается к узкому типу. Усечение типа.

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

При преобразовании из узкого типа в широкий тип проблем не возникает, и это преобразование никак не влияет на нас, не мешает нам жить. Это безопасное преобразование, потому что любому меньшему типу данных можно безопасно довыделить необходимое число байт.
При преобразовании из широкого типа в узкий тип проблемы возникают, потому что затирается часть информации. Теряются данные.
Нам, новичкам, бывает очень сложно понять и принять суть приведений типов. Если вы пользуетесь современным компилятором, то различать факт свершения расширения или сужения вам могут помочь предупреждения, вылезающие при компилировании программ. Хорошие компиляторы, как правило, имеют хорошую систему вывода информации об ошибках и предупреждениях. Предупреждения о сужении типа рекомендуется рассматривать как ошибку, потому что потеря данных ни к чему хорошему не приведёт. Но компиляторы не обязаны выдавать предупреждения, и нет какой-то единой системы вывода информации об опасностях, которым подвергается программа, создаваемая из написанного нами кода, поэтому в разных компиляторах в одном случае предупреждение может сработать, в другом не сработать. Сильно полагаться на систему предупреждений не стоит, но, если система срабатывает, то имеет смысл присмотреться к сообщению компилятора, возможно, где-то что-то не так.
Очень опасны сужающие преобразования:

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

Для ясности несколько псевдо-примеров сужающих преобразований:
int = float;8 = 10.988
char = long;‘Ъ’ = 1294867738
bool = int;

Почему их стоит считать сужающими, надеюсь, понятно. В int не может влезть весь float, в char не может влезть весь long, в bool не может влезть int, но тем не менее, компилятор разрешает подобные присваивания, а чтобы присваивание сработало, отсекается часть, которую невозможно переместить или скопировать в тип назначения.
Преобразования типов "одного племени" из широкого типа в узкий тип "собственного племени" не всегда являются сужающими, но они всегда потенциально-сужающие. Не всегда сужающие, как минимум потому, что на некоторых компьютерах ёмкости двух разных типов "одного племени" могут полностью совпадать. Под типами "одного племени" имются в виду близкие по природе типы: целые — из одного племени, с плавающей точкой — из другого.

При преобразовании из узкого в широкий, аналогично, — не всегда уширение.

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

Этот код, листинг #2, в компиляторах c 2011г., при включенной поддержке любого стандарта с 2011г., с очень большой вероятностью не скомпилируется. На самом деле такой код может быть скомпилирован, если ёмкость char равна ёмкости int.

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

Думается, что термины усечение и уширение намного лучше подходят общеприменимых терминов сужение и расширение.

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

Обратите внимание на листинг #5. Я уверен, что вы обычно так и задаёте или будете задавать значения. Стоит понимать, что здесь получается немного не так всё, как видится.

В моём случае компилятор взял на себя больше, чем было нужно, и молча создал дополнительную переменную типа float.
Незримо для нас произошла целая цепочка событий:

0.9 компилятором воспринялось как значение типа double

Поскольку во всём процессе вычисления в каждой вычислительной фазе все типы, участники действия, должны оказаться одинаковыми, компилятор создаёт новую переменную-клон, которая получает себе тип float. Доминирующий тип компилятором определяется типом конечного результата вычислительной фазы. В моём примере всего одна вычислительная фаза, а конечный тип этой фазы — тип переменной, в которую происходит присваивание. После того, как была создана переменная-клон, вычисление происходит с ней, а так как и левая, и правая часть имеют одинаковые типы, то всё окей.
Сложно написано, да? Поэтому приведу обычный арифметический пример с целыми и нецелыми числами.

Компилятор ничего не выведет на экран, и никакой полезной работы такое включение в программу не произведёт. Но само вычисление произойдёт. Да и удобно объяснять процесс вычислительного действия и что имеется в виду под вычислительной фазой в тексте этой статьи.

Вычислительная фаза — это мой авторский термин. Обозначает непосредственное действие двух участников. Например, в сумме — первое и второе слагаемое, в присваивании — источник и объект назначения. Любое вычисление в ПК раскладывается буквально на атомы. Т. е. показанный в коде арифметический пример будет разложени на множество действий:
1. 100.99 — 2
2. 9 * 98.99
3. 5 + 890.91
4. 895,91 + 33.2

В каждом действии два участника. А любое такое действие мной названо вычислительной фазой. В языке C++ участники любой вычислительной фазы должны иметь один и тот же тип. Если тип одного из участников отличен от типа второго, то компилятор будет создавать клон-калеку для подстановки в вычисление заместо оригинала. Тип клон-калеки выбирается компилятором таким образом, чтобы при возможности не терять информацию, т. е. компилятор без насильственных с нашей стороны ему указаний старается уширять типы. В показанном примере не указано никаких типов нами, поэтому компилятор не сводит никакую из фаз вычисления к типу, угодному нам.
Вычислительная фаза 1.
100.99 — 2
Какие здесь типы? double и int. Компилятор старается не потерять информацию и уширяет int, для чего создаёт новую переменную, давая ей тип double. Фактически ничего не уширяется, просто используется новая переменная, но есть эффект уширения переменной. В созданную переменную компилятор копирует каждый бит старой переменной, таким образом получается клон. Поскольку новая переменная шире старой, то копирование проходит без проблем: вся информация о старой переменной умещена в клон. Вычисление происходит с одинаковыми типами: double + double. Результат вычисления записывается в память, т. е. создаётся ещё одна временная переменная, тип компилятором для неё определён как double просто потому что double + double, элементарно, double. Таким образом компилятор выполнил первую вычислительную фазу, результатом которой оказалось число 98.99
Вычислительная фаза 2..
9 * 98.99
Какие здесь типы? int и double. Всё происходит по подобию первой фазы. Результатом вычислительной фазы оказывается число 890.91
Вычислительная фаза 3..
5 + 890.91
Какие здесь типы? int и double. Всё происходит по подобию первой фазы. Результатом вычислительно фазы оказывается число 895.91
Вычислительная фаза 4..
895.91 + 33.2
Какие здесь типы? double и double. Типы одинаковые, никаких преобразований не нужно. Клонов не создаётся. И сразу выполняется вычисление. Результатом оказывается число 929.11, тип которого, разумеется, double.

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

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

Добавляется пятая вычислительная фаза: присваивание:
Вычислительная фаза 5.
Какие здесь типы? float у переменной result и double у переменной, полученной при вычислении выражения, см. первые четыре фазы. Компилятор бы и рад расширить float до double, но здесь насильное принуждение компилятора нашим Васей Петиным выбирать тип float как доминирующий. Это потому что Вася Петин решил, каким должен быть результирующий тип, и явно его указал в окончание вычисления. Поэтому компилятор создаёт новую переменную, тип которой делает float, и в неё копирует всё из оригинала, тип которого double. Определённо есть вероятность, что в клон уместится не вся информация: если оригинал — большой чемодан, то клон сейчас — маленький чемодан: что-то может не влезть. Таким образом происходит усечение типа. Усекается тип правой части: из double создаётся float. В переменную назначения result, тип которой float копируется значение клона, тип которого float. В result в худшем случае оказывается исковерканное усечением значение.

На самом деле в показанном примере используются маленькие числа, к тому же литеральные константы, поэтому как такового сужения нет: скорее всего все числа, которые получились с типами double, уйдут во float без искажения. Этот пример показан для объяснения процесса, словами это объяснять немного проблематично. Стоит иметь в виду, что при определённых обстоятельствах всё именно так и произойдёт.
  • В момент присваивания нами некоторого значения в некоторую переменную компилятор насильно принуждается к выбору доминирующего типа: тип определяется типом переменной назначения.
Литеральные константы, которые представляют собой число с точкой, обычно воспринимаются компиляторами как значения с типом double. Литеральные константы — это цифры, символы или строки, представляющие собой значения, которые мы пишем непосредственно в исходном коде.

Результатом интерпретации написанного в исходном коде числа 2.5 как значения, тип которого double, произошло неявное преобразование: усечение. Из изначального double, который у 2.5, в переменной value оказалась только та часть, которую способен уместить тип int. Чемоданчик оказался мал.

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

В комментариях листинга #6 показано, что в коде происходит целых два неявных преобразования.
В строке 8 насильное принуждение к выбору доминирующего типа.
В строке 11 типы выравниваются для вычисления.
Всё происходит как в описанных раннее вычислительных фазах.
Вычислительная фаза 1.
Какие типы у участников вычислительной фазы value_x + value_y? double и float: уширение типа float, получается double
Вычислительная фаза 2.
Какие типы у участников вычислительной фазы присваивания? double и double, обычное копирование, без выёживаний компилятором.
  • Любое преобразование типа, явное или неявное, обозначает то, что компилятор создаст новую переменную: клон с дефектом, — которую будет подставлять вместо оригинала
Надеюсь, принцип численных преобразований стал ясен. Если вы уяснили суть, то мой многодневный труд не напрасен.
В голове некоторых моих читателей может появиться желание избежать или избегать некоторых неявных, т. е. невидымых нам, преобразований. Такое возможно. В случае с именованными переменными имеет смысл выбирать наиболее подходящие вычислению типы: если в вычислении получается double, то логично давать переменной под это вычисление такой же тип double. Это не очень сложно понять. В случае с литеральными константами тип самой константе задать можно не всегда, но в некоторых случаях возможно указать, какой тип мы хотим использовать:

Вычислительные фазы, надеюсь, понятны. Я очень много написал про процесс вычисления. Если всё понятно, то вы можете прикинуть, что происходит. Комментарии листинга #7 сейчас написаны как подсказки о происходящем.
Идём дальше.

  • C++ выполняет преобразование во время присваивания значения одного арифметического типа переменной, относящейся к другому арифметическому типу
  • C++ преобразует значения при комбинировании разных типов в выражениях
  • C++ преобразует значения при передаче аргументов функциям
  • Если вы не понимаете, что происходит во время автоматического преобразования типов, то некоторые результаты выполнения ваших программ могут оказаться несколько неожиданными
  • При написании программы следует избегать неопределенного поведения и преобразований, которые молча отбрасывают информацию. Компиляторы обычно способны предупредить о многих сомнительных преобразованиях.
С принципом приведения обычных числовых типов, надеюсь, вы разобрались. Немного отличается результат приведения, если тип, который рулит приведением, bool. В совсем старых компиляторах такого типа нет, но в компиляторах, которые появились после принятия стандарта языка, такой тип имеется. Этот тип относится к целочисленным типам. Так вот, если тип, который рулит приведением, bool, то при участии в вычислении значения, тип которого не bool, создаётся переменная-клон, тип которой bool. На словах всё страшно и нудно, поэтому переходим к примеру.

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

Кроме возникновения ошибки может выскочить предупреждение с намёком на 1998г. Это предупреждение сообщает о несовместимости списка инициализации (это фигурные скобки) со старыми версиями языков C++. Такое предупреждение в подавляющем большинстве случаев ошибкой не является.
  • Преобразования к типу bool стоят немного обособленно от других преобразований, проводимых с численными типами

    Связано это прежде всего с тем, что многие вопросы требуют ответа или да или нет. Существует ли функция?, равен ли результат сотне?, выбран ли красный цвет?, произошёл ли клик мышью? Поэтому bool компилятором определяется как тип, доминирующий над другими численными типами: если результатом вычисления является невыполненное условие, либо число 0, то булева переменная подберёт значение false, в любом другом случае в булевой переменной окажется true.
На самом деле тема неявных преобразований весьма громоздкая. Эта статья размещена в начале статейного цикла сайта, поэтому весь объём информации здесь захвачен не будет. Я описываю пока что только ту часть, незнание которой может мешать при написании ваших первых программ. Имейте это в виду.
В стандарте языка C++ неявным преобразованиям посвящён целый раздел. Специфика моего сайта предполагает, что читатель на данный момент со многими темами не знаком: тема неявных преобразований одна из первых по списку тем этого сайта, — поэтому в этой теме охвачена только некоторая часть весьма большой темы. Знакомство с неявными преобразованиями на очень ранней фазе обучения — мера вынужденная. Программист должен знать, чего следует ожидать от программы, чтобы не получать сюрпризов. Вот и приходится знакомить читателя поверхностно. К сожалению, мне не рассказывали о преобразованиях так подробно, поэтому я испытывал определённые затруднения очень долго.
В некоторых компиляторах C++ можно узнавать тип данных:

Разные компиляторы могут выводить на экран информацию о некотором конкретном типе в разном виде, но стандарт C++ гарантирует, что для одинаковых типов используемый компилятор будет выводит одинаковые строки.

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

2 комментария на «“С++. Неявные преобразования численных типов”»

  1. FeelTerr:

    ispravte, pojaluista:

    но если сравнивать с символом, то сравнивать надо с символом ’8′

    na

    но если сравнивать с символом, то сравнивать надо с символом '8'

    P.S.: spasibo za vash trud, i prostite za translit, net russkoi rasskladki i klaviatury 🙂

  2. Crew200:

    спасибо за такую сложную статью, но благодаря ей стало все же проще, чуть позже еще раз вернусь к этой тематике, что бы закрепить знания, пожалуй обширней на таком этапе, никто не расскажет !

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/andrey-borovskiy/qt4-7-prakticheskoe-programmirovanie-na-c-2/?lfrom=15589587
Яндекс.Метрика