8

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>

Valentin Kofman
  • 231
  • 1
  • 5

3 Answers3

2

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.

PaulR
  • 3,587
  • 14
  • 24
1

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());
Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • Why take the input by reference? This forces ODR-usage on the input, which forces the caller to have definitions for their constants (which could be omitted for class static constants). Also, it could potentially pessimize the runtime because of aliasing analysis "conservatism". I think copying the value is not a problem here – KABoissonneault Jul 24 '17 at 14:45
  • 2
    If you drop the `const` references, you run the risk of type conversion at the call site, which might not be desirable. Essentially programmers' choice though. – Bathsheba Jul 24 '17 at 14:46
0

You could start writing a wrapper for your integral types to match the exact type (or some conditions).

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

template <class...> struct conjunction : std::true_type {};
template <class B1> struct conjunction<B1> : B1 {};
template <class B1, class... Bn>
struct conjunction<B1, Bn...>
    : std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};

template <typename T> struct int_wrapper {
  explicit int_wrapper() : _val{T{}} {}

  explicit int_wrapper(const int_wrapper &other) : _val{other._val} {}

  template <typename U> explicit int_wrapper(U val) : _val{val} {
    static_assert(sizeof(T) >= sizeof(U), "Size mismatch.");
    static_assert(conjunction<std::is_signed<T>, std::is_signed<U>>::value,
                  "sign mismatch");
  }

  explicit operator T() { return _val; }
  explicit operator T() const { return _val; }

  T _val;
};

std::ostream &operator<<(std::ostream &stream, const int_wrapper<int16_t> &v) {
  stream << v._val;
  return stream;
}

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

  return 0;
}

clang on macOS gives you error like:

so.cpp:18:60: error: non-constant-expression cannot be narrowed from type 'unsigned long long' to 'short' in initializer list
      [-Wc++11-narrowing]
  template <typename U> explicit int_wrapper(U val) : _val{val} {
                                                           ^~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:1752:31: note: in
      instantiation of function template specialization 'int_wrapper<short>::int_wrapper<unsigned long long>' requested here
            ::new((void*)__p) _Up(_VSTD::forward<_Args>(__args)...);
                              ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:1668:18: note: in
      instantiation of function template specialization 'std::__1::allocator<int_wrapper<short> >::construct<int_wrapper<short>,
      unsigned long long>' requested here
            {__a.construct(__p, _VSTD::forward<_Args>(__args)...);}
                 ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:1514:14: note: in
      instantiation of function template specialization 'std::__1::allocator_traits<std::__1::allocator<int_wrapper<short> >
      >::__construct<int_wrapper<short>, unsigned long long>' requested here
            {__construct(__has_construct<allocator_type, _Tp*, _Args...>(),
             ^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/vector:1643:25: note: in
      instantiation of function template specialization 'std::__1::allocator_traits<std::__1::allocator<int_wrapper<short> >
      >::construct<int_wrapper<short>, unsigned long long>' requested here
        __alloc_traits::construct(this->__alloc(),
                        ^
so.cpp:37:5: note: in instantiation of function template specialization 'std::__1::vector<int_wrapper<short>,
      std::__1::allocator<int_wrapper<short> > >::emplace_back<unsigned long long>' requested here
  v.emplace_back(std::numeric_limits<uint64_t>::max());
    ^
so.cpp:18:60: note: insert an explicit cast to silence this issue
  template <typename U> explicit int_wrapper(U val) : _val{val} {
                                                           ^~~
                                                           static_cast<short>( )
so.cpp:19:5: error: static_assert failed "Not the same size."
    static_assert(sizeof(T) >= sizeof(U), "Not the same size.");
    ^             ~~~~~~~~~~~~~~~~~~~~~~
so.cpp:20:5: error: static_assert failed "sign mismatch"
    static_assert(conjunction<std::is_signed<T>, std::is_signed<U>>::value,
    ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 errors generated.
Tareq A. Siraj
  • 424
  • 3
  • 9