C++11: "Narrowing Conversion Inside { }" with Modulus

C++11: narrowing conversion inside { } with modulus

In this specific case making id const or constexpr will fix the problem:

constexpr unsigned int id = 100;

since there is an exception for the case where you have a constant expression whose result after conversion will fit into the target type.

In the more general case you may also use static_cast to cast the result to unsigned char:

{ static_cast<unsigned char>( id % 3), static_cast<unsigned char>( id % 5) }
^^^^^^^^^^^ ^^^^^^^^^^^

We can find he exception for constant expressions and narrowing conversions in the draft C++ standard section 8.5.4 List-initialization which says:

A narrowing conversion is an implicit conversion

and include the following bullet (emphasis mine):

  • from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except
    where the source is a constant expression whose value after integral
    promotions will fit into the target type
    .

Note, the wording changed from the original draft C++11 standard to what I quote above due to defect report 1449.

Why gcc warns about narrowing conversion only for uniform initialization?

Because the standard says, narrowing conversions limit is specified only for list initialization (since C++11).

list-initialization limits the allowed implicit conversions by
prohibiting the following:

  • conversion from a floating-point type to an integer type
  • conversion from a long double to double or to float and conversion from double to float, except where the source is a constant expression
    and overflow does not occur
  • conversion from an integer type to a floating-point type, except where the source is a constant expression whose value can be stored
    exactly in the target type
  • conversion from integer or unscoped enumeration type to integer type that cannot represent all values of the original, except where source
    is a constant expression whose value can be stored exactly in the
    target type

For the other initialization methods (using parentheses or equal sign), narrowing conversions limit rule is not applied (added); because that might break much legacy code.

warning: narrowing conversion C++11

First, why narrowing? That comes from §5/10:

Many binary operators that expect operands of arithmetic or enumeration type cause conversions and yield result types in a similar way. The purpose is to yield a common type, which is also the type of the result. This pattern is called the usual arithmetic conversions, which are defined as follows:

— [..]

— Otherwise, the integral promotions (4.5) shall be performed on both operands.

where the integral promotion is defined in 4.5/1:

A prvalue of an integer type other than bool, char16_t, char32_t, or wchar_t whose integer conversion rank (4.13) is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

In our case then, we have decltype(char + char) is int because char's conversion rank less than int so both are promoted to int before the call to operator+. Now, we have ints that we're passing to a constructor that takes chars. By definition (§8.5.4/7, specifically 7.4):

A narrowing conversion is an implicit conversion

(7.4) — from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type.

which is explicitly prohibited in list-initialization specifically as per §8.5.4/3 (emphasis mine, the "see below" actually refers to what I just copied above):

List-initialization of an object or reference of type T is defined as follows

— [..]

— Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed. [...]

This is why your vec3<T>{int, int, int} gives you a warning: the program is ill-formed due to integer promotion requiring a narrowing conversion on all the expressions. Now, the statement about "ill-formed" specifically arises only in the context of list-initialization. This is why if you initialize your vector without {}s, you do not see that warning:

vec3<T> operator-(const vec3<T> &other) { 
// totally OK: implicit conversion from int --> char is allowed here
return vec3<T>( x - other.x, y - other.y, z - other.z );
}

As to solving this problem - just calling the constructor without list-initialization is probably the simplest solution. Alternatively, you can continue to use list-initialization and just template your constructor:

template <typename A, typename B, typename C>
vec3(A xx, B yy, C zz)
: x(xx) // note these all have to be ()s and not {}s for the same reason
, y(yy)
, z(yy)
{ }

Is g++ falsely throwing -Wnarrowing?

Using braces prefers the list constructor (the one taking std::initializer_list<char> here, number 9 on cppreference). In this constructor, the elements are char, so it is a narrowing conversion. GCC will give you an actual error for this instead of only a warning if you compile with -pedantic-errors; this is not a conforming program because narrowing conversions are not allowed in braced initialization.

Narrowing conversion from int to unsigned char

The operands to your addition expression are undergoing integral promotion.

In particular, arithmetic operators do not accept types smaller than int as arguments, and integral promotions are automatically applied after lvalue-to-rvalue conversion, if applicable. This conversion always preserves the value.

Your a and b are being promoted to int or unsigned int, added, and then converted back down to unsigned char.

If the conversion is expected program behavior (from your point of view as the designer), you can explicitly cast it to the type you want. An explicit cast is not a narrowing conversion. An implicit cast is. So if we change the implicit cast to an explicit cast, the program is no longer ill-formed.

T t = { static_cast<unsigned char>(a + b) };

Hidden narrowing conversion from int to uint8_t

You were close with this:

uint8_t mask{static_cast<uint8_t>(a.get_a()) | static_cast<uint8_t>(aa.get_a())};

But a.get_a() and aa.get_a() are already uint8_t, so the cast does nothing.

It's the | operation that:

  • converts both operands to int (after you can do anything about it)
  • evaluates to an int

So it's the whole expression you now need to subsequently convert:

uint8_t mask{static_cast<uint8_t>(a.get_a() | aa.get_a())};

You were also right to try dropping the {}-initialisation, which is probably what I'd do too. You just don't need its strictness here.

uint8_t mask = a.get_a() | aa.get_a();

This is clear, concise and correct.

Why is g++ saying about ill-formed narrowing when it's clearly well-defined in given case?

I believe it would put them together and still get a 32 bit int. Yes, the values would technically fit into a char, but it's the type of the output from the operation, and not the data within. Note that both test and 0xff would be 32 bits. Try casting the output:

(unsigned char)(test & 0xff)

error: narrowing conversion of ‘199’ from ‘int’ to ‘char’ inside { } [-Wnarrowing]

The lowest-effort way to get your thing to build is probably to add -Wno-narrowing to your compiler invocation. If you're using make, you can probably start it with something like CFLAGS=-Wno-narrowing make (assuming you're using bash) to get the desired effect.



Related Topics



Leave a reply



Submit