warning: narrowing conversion C++11
Asked Answered
S

2

29

g++ 4.9.0 -O2 -std=c++11

template<class T>
struct vec3 {
    T x, y, z;
    vec3() = default;
    vec3(const vec3<T> &other) = default;
    vec3(T xx, T yy, T zz) { x = xx; y = yy; z = zz; }
    vec3<T> operator-(const vec3<T> &other) { 
      return vec3<T>{ x - other.x, y - other.y, z - other.z }; 
    }
};

int main() {
    vec3<char> pos{ 0, 0, 0 };
    vec3<char> newPos{ 0, 0, 0 };
    auto p = pos - newPos;

    return 0;
}

I get the warning :

!!warning: narrowing conversion of ‘(((int)((vec3<char>*)this)->vec3<char>::x) - ((int)other.vec3<char>::x))’ from ‘int’ to ‘char’ inside { } [-Wnarrowing]

But if I do it with (...) insted of {...} inside the operator- function the warning disappears. Why?

Scrunch answered 8/1, 2015 at 16:30 Comment(3)
Have a look at the discussion in this GCC bug report, especially Jonathan Wakely's responseFasano
{} catches narrowing conversions (making the code ill-formed). () doesn't. Or are you asking why it's narrowing?Barragan
Yes, why is it narrowing?Scrunch
O
25

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)
{ } 
Objection answered 10/1, 2015 at 14:12 Comment(0)
M
19

A couple of things are going on here. First, the {...} syntax prohibits implicit narrowing conversions. So the easy fix is to change curly brackets to parentheses:

vec3<T> operator-(const vec3<T> &other) { 
  return vec3<T>( x - other.x, y - other.y, z - other.z ); 
}

The second thing going on is, "eh? char minus a char is a char, what is the problem?!" And the answer here is that C/C++ want to use natural size for arithmetic operations. That is why you see the (int) cast in your error message. Here is a good explanation of why it does that (just in case that StackOverflow answer ever disappears, he's quoting 6.3.1.1 of the C11 standard).

So, the other way to fix your code is:

vec3<T> operator-(const vec3<T> &other) { 
  return vec3<T>{
    static_cast<char>(x - other.x),
    static_cast<char>(y - other.y),
    static_cast<char>(z - other.z)
    };
}

By the way, item 7 in Effective Modern C++ convinced me that there are times when () is better to initialize with, and there are times when {} is better. Sometimes you have to just shrug and use the other one.

Mirnamirror answered 9/1, 2015 at 20:34 Comment(1)
Clear and concise, explained exactly why I was getting an error trying to initialize a uint16_t member with the addition of 2 uint16_ts. (eg Foo foo {x +y}).Aeolipile

© 2022 - 2024 — McMap. All rights reserved.