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 int
s that we're passing to a constructor that takes char
s. 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)
{ }