В арифметическом выражении типы операндов могут быть преобразованы к общему типу. Такие преобразования описаны в стандарте языка — в C# они существенно проще чем в C++. Тем не менее, скорее всего далеко не каждый программист знает обо всех тонкостях.
Возможно у вас были случаи, когда тип арифметического выражения оказывался не таким как вы предполагали? Насколько хорошо вы знаете стандарт языка? Предлагаю проверить себя, заменив auto и var на правильный тип в этих выражениях и заодно вычислить результат:
C++ (будем считать, что используется модель данных LP64):
C#:
Ответ под картинкой
С++ (LP64):
C#:
Из этого теста, а если точнее, из стандартов языка С++ и C# следует:
1. Вычисление x. В арифметическом выражении все переменные, значения которых представимы типом int, будут преобразованы к типу int. Поэтому при сложении двух переменных типа char, unsigned char, short int, unsigned short int в C++ или переменных типа byte, sbyte, short, ushort в C# результат будет иметь тип int и переполнения не произойдёт. В наших примерах переменная x примет значение 510.
2. Вычисление y. Если оба аргумента имеют тип int дальнейшего расширения типов происходить уже не будет и здесь уже возможно переполнение. В C++ это неопределённое поведение. В C# по умолчанию в случае переполнения приложение продолжит работу. Используя ключевое слово checked или флаг компилятора /checked можно изменить поведение приложения чтобы в случае переполнения бросался OverflowException. В нашем тесте переменная y примет значение -2 и в C++ и в C#. Хотя ещё раз повторю, что в С++ мы имеем неопределённое поведение, результом которого может быть что угодно, например в y может быть записано число 100500 или произойдёт переполнение стека.
3. Вычисление z. Если один из аргументов имеет тип int, а другой unsigned int в C++ или uint в C#, то здесь стандарты двух языков написаны по-разному! В С++ оба аргумента будут преобразованы к типу unsigned int, кстати здесь уже не будет неопределённого поведения при переполнении. В C# оба аргумента будут преобразованы к типу long и переполнения не произойдёт ни при каких условиях. Поэтому мы и получили в наших программах на разных языках, разные значения для переменной z.
Рассмотрим теперь какие ошибки может содержать код, который написан без учёта правильного преобразования типов.
Пример на С++:
Это пример кода из проекта IPP Samples. При сравнении результата выражения с нулём нужно помнить о том что int может быть преобразован в unsigned int, а long — в unsigned long. В нашем случае выражение m_iCurrMBIndex — x будет иметь тип unsigned int, и поэтому оно всегда неотрицательно, о чём сообщит PVS-Studio: V547 Expression 'm_iCurrMBIndex — x < 0' is always false. Unsigned type value is never < 0.
Пример на C#:
Это пример из проекта SpaceEngineers. В C# нужно помнить о том, что при сложении двух переменных типа int расширения типа до long не произойдёт в отличие от сложения переменной типа int и переменной типа uint. Поэтому здесь в переменную num будет записано значение типа int, которое всегда удовлетворяет условию num <= 0x7fffffffL. PVS-Studio знает об этом и выдаёт сообщение V3022 Expression 'num <= 0x7fffffffL' is always true.
Хорошо, когда знаешь стандарт и не допускаешь подобные ошибки, но на практике постоянно помнить все тонкости языка сложно, а в случае с C++ вообще нереально. Поэтому полезно пользоваться статическими анализаторами, например, PVS-Studio.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ilya Ivanov. Type Conversion in C++ and C# Arithmetic Expressions.
Возможно у вас были случаи, когда тип арифметического выражения оказывался не таким как вы предполагали? Насколько хорошо вы знаете стандарт языка? Предлагаю проверить себя, заменив auto и var на правильный тип в этих выражениях и заодно вычислить результат:
C++ (будем считать, что используется модель данных LP64):
void Test()
{
unsigned char c1 = std::numeric_limits<unsigned char>::max();
unsigned char c2 = std::numeric_limits<unsigned char>::max();
int i1 = std::numeric_limits<int>::max();
int i2 = std::numeric_limits<int>::max();
unsigned int u1 = std::numeric_limits<unsigned int>::max();
auto x = c1 + c2;
auto y = i1 + i2;
auto z = i1 + u1;
}
C#:
void Test()
{
byte b1 = byte.MaxValue;
byte b2 = byte.MaxValue;
int i1 = int.MaxValue;
int i2 = int.MaxValue;
uint u1 = uint.MaxValue;
var x = b1 + b2;
var y = i1 + i2;
var z = i1 + u1;
}
Ответ под картинкой
С++ (LP64):
int x = c1 + c2; // = 510
int y = i1 + i2; // = -2
unsigned int z = i1 + u1; // = 2147483646
C#:
int x = b1 + b2; // = 510
int y = i1 + i2; // = -2
long z = i1 + u1; // = 6442450942
Из этого теста, а если точнее, из стандартов языка С++ и C# следует:
1. Вычисление x. В арифметическом выражении все переменные, значения которых представимы типом int, будут преобразованы к типу int. Поэтому при сложении двух переменных типа char, unsigned char, short int, unsigned short int в C++ или переменных типа byte, sbyte, short, ushort в C# результат будет иметь тип int и переполнения не произойдёт. В наших примерах переменная x примет значение 510.
2. Вычисление y. Если оба аргумента имеют тип int дальнейшего расширения типов происходить уже не будет и здесь уже возможно переполнение. В C++ это неопределённое поведение. В C# по умолчанию в случае переполнения приложение продолжит работу. Используя ключевое слово checked или флаг компилятора /checked можно изменить поведение приложения чтобы в случае переполнения бросался OverflowException. В нашем тесте переменная y примет значение -2 и в C++ и в C#. Хотя ещё раз повторю, что в С++ мы имеем неопределённое поведение, результом которого может быть что угодно, например в y может быть записано число 100500 или произойдёт переполнение стека.
3. Вычисление z. Если один из аргументов имеет тип int, а другой unsigned int в C++ или uint в C#, то здесь стандарты двух языков написаны по-разному! В С++ оба аргумента будут преобразованы к типу unsigned int, кстати здесь уже не будет неопределённого поведения при переполнении. В C# оба аргумента будут преобразованы к типу long и переполнения не произойдёт ни при каких условиях. Поэтому мы и получили в наших программах на разных языках, разные значения для переменной z.
Рассмотрим теперь какие ошибки может содержать код, который написан без учёта правильного преобразования типов.
Пример на С++:
typedef unsigned int Ipp32u;
typedef signed int Ipp32s;
Ipp32u m_iCurrMBIndex;
VC1EncoderMBInfo* VC1EncoderMBs::GetPevMBInfo(Ipp32s x, Ipp32s y)
{
Ipp32s row = (y > 0) ? m_iPrevRowIndex : m_iCurrRowIndex;
return ((m_iCurrMBIndex - x < 0 || row < 0)
? 0 : &m_MBInfo[row][m_iCurrMBIndex - x]);
}
Это пример кода из проекта IPP Samples. При сравнении результата выражения с нулём нужно помнить о том что int может быть преобразован в unsigned int, а long — в unsigned long. В нашем случае выражение m_iCurrMBIndex — x будет иметь тип unsigned int, и поэтому оно всегда неотрицательно, о чём сообщит PVS-Studio: V547 Expression 'm_iCurrMBIndex — x < 0' is always false. Unsigned type value is never < 0.
Пример на C#:
public int Next(int minValue, int maxValue)
{
long num = maxValue - minValue;
if (num <= 0x7fffffffL)
{
return (((int)(this.Sample() * num)) + minValue);
}
return (((int)((long)(this.GetSampleForLargeRange() * num)))
+ minValue);
}
Это пример из проекта SpaceEngineers. В C# нужно помнить о том, что при сложении двух переменных типа int расширения типа до long не произойдёт в отличие от сложения переменной типа int и переменной типа uint. Поэтому здесь в переменную num будет записано значение типа int, которое всегда удовлетворяет условию num <= 0x7fffffffL. PVS-Studio знает об этом и выдаёт сообщение V3022 Expression 'num <= 0x7fffffffL' is always true.
Хорошо, когда знаешь стандарт и не допускаешь подобные ошибки, но на практике постоянно помнить все тонкости языка сложно, а в случае с C++ вообще нереально. Поэтому полезно пользоваться статическими анализаторами, например, PVS-Studio.
Дополнительные ссылки
- Преобразование типов в арифметических выражениях в C++.
- Преобразование типов в арифметических выражениях в C#.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ilya Ivanov. Type Conversion in C++ and C# Arithmetic Expressions.
Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.
Комментарии (7)
Greendq
29.03.2016 20:19-1Вот мне интересно, зачем было вводить тип auto в С++? :)
0xd34df00d
30.03.2016 00:49+2В C++ нет типа auto.
SemihalDevs
30.03.2016 09:51типа нету… А вот ключевое слово, позволяющее определять тип переменной — есть.
Greendq
30.03.2016 11:03Пардон, не так выразился. Ключевое слово, да, но суть от этого не меняется — так как отдаётся всё на усмотрение компилятора.
0xd34df00d
30.03.2016 15:38Напишете вы
auto r = 3 / 2;
или
double r = 3 / 2;
результат будет один и тот же, в r будет лежать вполне себе округлённый результат деления.
Так же и в вышеприведённых примерах.
Никакого усмотрения калькулятора, тип правой части от левой не зависит.
win32asm
30.03.2016 09:51+2Чтобы не писать что-то типа map::const_iterator каждый раз, когда надо что-то найти?
Да, я знаю, можно обойти typedefом. Но на typedefы внутри шаблонных функций могут кончиться «хорошие» имена — и опять полезут длинные и странные названия типов, от которых хотели уйти.
shai_hulud
https://msdn.microsoft.com/en-us/library/aa691330%28v=vs.71%29.aspx
Вот правила "расширения" числовых типов в C#, если кому интересен алгоритм
Плюс везде, где написано "compile-time error occurs", это не совсем правда. Если операнд может быть преобразован к требуемому типу на этапе компиляции, то ошибки не будет.
Бонусом статья про Null-lifting, https://blogs.msdn.microsoft.com/ericlippert/2007/06/27/what-exactly-does-lifted-mean/