Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forbid integer conversion with precision loss

How to prevent such code from compiling?

#include <vector>
#include <limits>
#include <iostream>
#include <cstdint>

int main() {
  std::vector<int16_t> v;
  v.emplace_back(std::numeric_limits<uint64_t>::max());
  std::cout << v.back() << std::endl;
  return 0;
}

g++ and clang with -std=c++14 -Wall -Wextra -Werror -pedantic -Wold-style-cast -Wconversion -Wsign-conversion don't even warn about it. The example also compiles without warnings with std::vector<uint16_t>

like image 873
Valentin Kofman Avatar asked Jul 24 '17 14:07

Valentin Kofman


People also ask

Why do we lose precision when we convert from decimal to binary?

We get this loss of precision all the time in our computing, because our numbers are being converted from decimal into binary floating point. Many things that look fine in decimal, such as 0.1 or 0.4, are repeating decimals in binary. In binary, 0.5 has a lovely representation: 0.1.

How to retrieve all the fractional bits of an integer?

Fractional bits are reserved according to the quantization step that you choose. Another workaround is to float (4 bytes) to int (4 bytes). If the converted integer is in range of short values, then all bits can be retrieved. Float to Int to Short : short n = (short) (Float.floatToRawIntBits (x)); Hope that this will be of help to you.

How does binary coding affect the precision of floating point calculations?

To resolve the behavior, most programmers either ensure that the value is greater or less than what is needed, or they get and use a Binary Coded Decimal (BCD) library that will maintain the precision. Binary representation of floating-point values affects the precision and accuracy of floating-point calculations.


2 Answers

Add -Wsystem-headers to the command line. Among the many spurious warnings you will find the desired warning.

In file included from (...)include/c++/6.3.0/x86_64-w64-mingw32/bits/c++allocator.h:33:0,
                 from (...)include/c++/6.3.0/bits/allocator.h:46,
                 from (...)include/c++/6.3.0/vector:61,
                 from test.cpp:1:
(...)include/c++/6.3.0/ext/new_allocator.h: In instantiation of 'void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = short int; _Args = {long long unsigned int}; _Tp = short int]':
(...)include/c++/6.3.0/bits/alloc_traits.h:455:4:   required from 'static void std::allocator_traits<std::allocator<_Tp1> >::construct(std::allocator_traits<std::allocator<_Tp1> >::allocator_type&, _Up*, _Args&& ...) [with _Up = short int; _Args = {long long unsigned int}; _Tp = short int; std::allocator_traits<std::allocator<_Tp1> >::allocator_type = std::allocator<short int>]'
(...)include/c++/6.3.0/bits/vector.tcc:96:30:   required from 'void std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {long long unsigned int}; _Tp = short int; _Alloc = std::allocator<short int>]'
test.cpp:9:54:   required from here
(...)include/c++/6.3.0/ext/new_allocator.h:120:4: error: conversion to 'short int' from 'long long unsigned int' may alter its value [-Werror=conversion]
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^

I know this is not a real solution, although it technically answers the question.

The problem is, that emplace_back forwards all the arguments, in this case the uint64_t to the constructor of the contained type. First the argument to emplace_back is deduced as uint64_t. No conversion happens at the call of emplace back. The narrowing conversion then happens "inside" the emplace_back implementation in the system header. The compiler does not know that this is the fault of the caller and suppresses the warning because it is in a system header.

like image 52
PaulR Avatar answered Sep 17 '22 20:09

PaulR


I solve this by templates and specialisation:

 template<
        typename T/*the desired type*/,
        typename Y/*the source type*/
    > T integral_cast(const Y& y)
    {
        static_assert(false, "undefined integral_cast");
    }

which I then specialise at leisure if I want the cast to work:

// Pass through for uint32_t
    template<>
    inline std::uint32_t integral_cast(const uint32_t& y)
    {
        return y;
    }

and

// Specialisation to convert std::uint32_t to double
    template<>
    inline double integral_cast(const std::uint32_t& y)
    {
        double ret = static_cast<double>(y); // this never loses precision under IEEE754
        return ret;
    }

At the point of use you write code of the form

int16_t y = integral_cast<int16_t>(std::numeric_limits<uint64_t>::max());
like image 34
Bathsheba Avatar answered Sep 19 '22 20:09

Bathsheba