mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
189 lines
11 KiB
Markdown
189 lines
11 KiB
Markdown
# Сужающие преобразования и неявное приведение типов
|
||
|
||
Неявные преобразования типов запрещены во многих современных языках программирования, особенно новых.
|
||
|
||
Так в Rust, Haskell, Kotlin нельзя просто так использовать `float` и `int` в одном арифметическом выражении, без явного указания преобразовать одно в другое. Python не так строг, но все же не дает смешивать строки, символы и числа.
|
||
|
||
В С++ запрета неявного преобразования нет, что порождает массу ошибочного кода. Причем в таком коде может быть как определенное, но неожиданное, так неопределенное поведение.
|
||
|
||
Пример:
|
||
|
||
```C++
|
||
#include <vector>
|
||
#include <numeric>
|
||
#include <iostream>
|
||
|
||
int average(const std::vector<int>& v) {
|
||
if (v.empty()) {
|
||
return 0;
|
||
}
|
||
return std::accumulate(v.begin(), v.end(), 0) / v.size();
|
||
}
|
||
|
||
int main() {
|
||
std::cout << average({-1,-1,-1});
|
||
}
|
||
```
|
||
|
||
Любой, кто мельком бросит взгляд на этот код, будет ожидать, что результатом работы окажется `-1`.
|
||
Но, увы, результат будет совершенно [другим](https://godbolt.org/z/6GEs4q).
|
||
|
||
В этом коде нет неопределенного поведение (по крайней мере на используемых входных данных). Но есть неявное приведение типов, делающее результат неожиданным.
|
||
|
||
1. Тип возвращаемого значения `std::accumulate` определяется третьим аргументом. В данном случае это целочисленный знаковый ноль — тип по умолчанию для всех числовых литералов.
|
||
2. Тип возвращаемого значения операции деления определяется наибольшим из участвующих типов аргументов, а также правилами [integer promotion](https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules). В примере тип левого аргумента — `int`, а правого — `size_t` — достаточно широкое беззнаковое целое, Более широкое чем `int`. Потому, по правилам integer promotion, результатом будет `size_t`
|
||
3. `-3` неявно преобразуется к типу `size_t` — такое преобразование вполне определено. Результатом будет беззнаковое число `2^N - 3`.
|
||
4. Далее будет произведено деление беззнаковых чисел. `(2^N - 3) / 3`. Старший бит результата окажется нулевым.
|
||
5. Возвращаемым типом функции `average` объявлен `int`. Так что нужно выполнить еще одно неявное преобразование.
|
||
6. В общем случае преобразование unsigned -> signed определяется реализацией (implementation defined).
|
||
1. Если размеры типов `int` и `size_t` одинаковые, то, поскольку старший бит нулевой, положительное число укладывается в допустимый диапазон значений для типа `int` — стандарт гарантирует, что никаких проблем нет
|
||
2. Если размеры не совпадают, то произойдет сужающее преобразование (_narrowing conversion_), которое как раз таки отдано на откуп деталям реализации. Так, вместо ожидаемой обрезки старших, не поместившихся, битов, на некоторых платформах может произойти замена на `std::numeric_limits<int>::max`
|
||
3. Для примера сборки под 64-битную платформу с помощью `gcc` сужающее преобразование определено, как и ожидается, через обрезку старших битов. Поэтому итоговым результатом оказывается (`(2^64 -3) / 3 % 2^32` )
|
||
|
||
|
||
Неявные приведения типов касаются не только встроенных примитивов, но и более сложных типов. И самое неприятное — они вмешиваются в выбор подходящей перегрузки функции, приводя к различным, часто неприятным, казусам.
|
||
|
||
Пример с [`abs`](https://godbolt.org/z/KbTza4)
|
||
```C++
|
||
#include <cmath>
|
||
#include <iostream>
|
||
|
||
int main() {
|
||
std::cout << abs(3.5) << "\n"; // функция библиотеки С,
|
||
// принимает на вход тип long
|
||
// результат — 3
|
||
std::cout << std::abs(3.5); // функция библиотеки С++
|
||
// перегружена для double
|
||
// результат — 3.5
|
||
}
|
||
```
|
||
|
||
Еще более неприятный пример наблюдается со стандартным типом `std::string`
|
||
|
||
```C++
|
||
#include <string>
|
||
|
||
int main() {
|
||
std::string s;
|
||
s += 48; // неявное приведение к char.
|
||
s += 1000; // а тут еще и с переполнением, очень неприятным
|
||
// на платформе с signed char.
|
||
s += 49.5; // опять-таки неявное приведение к char
|
||
}
|
||
```
|
||
Этот ужас [компилируется](https://godbolt.org/z/K4WhTe)!
|
||
|
||
Казалось бы, этот пример совершенно ужасного использования никогда не может встретиться в нормальном коде. Увы, но может.
|
||
|
||
Вы можете написать обобщенный код своего `std::accumulate`, с различными проверками шаблонных аргументов, и случайно, по ошибке, передать в него `string` в качестве аккумулятора и контейнер, например, `float`. И никакой ошибки компиляции [не будет](https://godbolt.org/z/8Y7xe8). Только странный баг в программе.
|
||
|
||
---
|
||
|
||
Цепочки неявных преобразований могут быть очень неочевидными
|
||
|
||
```C++
|
||
void f(float&& x) { std::cout << "float " << x << "\n"; }
|
||
void f(int&& x) { std::cout << "int " << x << "\n"; }
|
||
void g(auto&& v) { f(v); } // C++20
|
||
// template <class T> void g(T v) { f(v); }
|
||
int main() {
|
||
g(2);
|
||
g(1.f);
|
||
}
|
||
```
|
||
|
||
Самым удивительным образом этот пример [выводит](https://godbolt.org/z/s1933K)
|
||
```
|
||
float 2
|
||
int 1
|
||
```
|
||
Хотя мы подставляли типы констант совсем наоборот и почти наверняка ожидали
|
||
```
|
||
int 2
|
||
float 1
|
||
```
|
||
|
||
Это не баг компилятора и не неопределенное поведение! Всему виной хитрая цепочка неявных
|
||
преобразований.
|
||
|
||
Рассмотрим ее на примере первого вызова `g(2)`, подставив параметр шаблона
|
||
```C++
|
||
void g(int&& v) {
|
||
// Несмотря на то что тип v — int&&
|
||
// Дальнейшее использование v в выражениях дает int& !
|
||
// decltype(v) == int&&
|
||
// decltype((v)) == int&
|
||
|
||
// Функции f принимают только rvalue ссылки
|
||
|
||
// Неявное преобразование int& к int&& запрещено
|
||
// int&& x = 5;
|
||
// int&& y = x; // не компилируется!
|
||
|
||
// Таким образом перегрузка f(int&&) не может быть использована
|
||
|
||
// Остается f(float&&)
|
||
// int умеет неявно приводиться к float
|
||
// int& умеет неявно выступать в роли просто int
|
||
// неявный static_cast<float>(v) возвращает временное значение float
|
||
// временные значения типа T неявно биндятся к T&&
|
||
|
||
// Имеем цепочку преобразований:
|
||
// int& -> int -> float -> float&&
|
||
|
||
f(v); // будет вызван f(float&&) !
|
||
|
||
// явно: f(static_cast<float>(v));
|
||
}
|
||
```
|
||
|
||
Конечно, никто никогда (по крайней мере явно) не принимает примитивы по `rvalue`-ссылкам. Потому что это бессмысленно. Но даже без `rvalue`-ссылки для примитивов, мы можем сотворить нечто ужасное
|
||
|
||
```C++
|
||
struct MyMovableStruct {
|
||
operator bool () {
|
||
return !data.empty();
|
||
}
|
||
std::string data;
|
||
};
|
||
|
||
void consume(MyMovableStruct&& x) {
|
||
std::cout << "MyStruct: " << x.data << "\n";
|
||
}
|
||
void consume(bool x) { std::cout << "bool " << x << "\n"; }
|
||
void g(auto&& v) { consume(v); }
|
||
int main() {
|
||
g(MyMovableStruct{"hello"});
|
||
}
|
||
```
|
||
|
||
Той же самой цепочкой преобразований [получим](https://godbolt.org/z/bncnPj) в выводе `bool 1`.
|
||
Разве что последний шаг не нужен.
|
||
|
||
|
||
---
|
||
|
||
Обязательно включайте предупреждения компилятора обо всех неявных преобразованиях. Очень желательно трактовать их как ошибки.
|
||
|
||
Не привносите неявные преобразования для своих типов — всегда помечайте однопараметрические конструкторы как `explicit`.
|
||
|
||
Если перегружаете операторы приведения (`operator T()`) для своих типов — также делайте их `explicit`.
|
||
|
||
Если ваши функции/методы рассчитаны на работу только с определенным примитивным типом, навешивайте на них ограничения с помощью шаблонов, SFINAE, концептов, или, что [очень просто](https://godbolt.org/z/Yx1e3d), механизма явного удаления перегрузок (`= delete`):
|
||
|
||
```C++
|
||
int only_ints(int x) { return x;}
|
||
|
||
template <class T>
|
||
auto only_ints(T x) = delete;
|
||
|
||
int main() {
|
||
const int& x = 2;
|
||
only_ints(2);
|
||
only_ints(x);
|
||
char c = '1';
|
||
only_ints(c); // Compilation Error.
|
||
only_ints(2.5); // Explicitly deleted
|
||
}
|
||
```
|