Does the standard prevent narrowing conversion of literal with small enough literal values within variadic templates

Viewed 233

Here is the minimal example:

#include <array>

template <class... T>
constexpr std::array<unsigned char, sizeof...(T)> act(T... aArgs)
{
    return std::array<unsigned char, sizeof...(T)>{aArgs...};
}

int main()
{
    act(5, 5);
}

EDIT Where GCC and Clang can compile that snippet without a complaint No they cannot.

The latest MSVC fails with:

<source>(6): error C2397: conversion from 'int' to '_Ty' requires a narrowing conversion

    with

    [

        _Ty=unsigned char

    ]

See: https://godbolt.org/z/1PmeLk


  • Since in this situation, the compiler has everything it needs to statically verify that the call to act(5, 5) does not overflow for the provided values, is it a standard-compliant behaviour to fail on this code?

Bonus question:

  • Since there is no literal suffix to get an unsigned char literal, how to workaround this bug || fix this non-standard code?
2 Answers

Since in this situation, the compiler has everything it needs to statically verify that the call to act(5, 5) does not overflow for the provided values, is it a standard-compliant behaviour to fail on this code?

Yes. The compiler is only allowed to do a narrowing conversion when it can guarantee that the value is representable in narrower type. If you had

std::array<unsigned char, 2> foo = {5, 127};

then this would be okay because the compiler knows 5 and 127 are representable. Your case though is not the same. You do your initialization inside a function and inside the function {aArgs...} does not have the same guarantee since it is not a constant expression (no variables passed to a function are constant expressions). Because of this the compiler can't prove in all cases that it will be valid so it issues a warning/error.

Since there is no literal suffix to get an unsigned char literal, how to workaround this bug || fix this non-standard code?

You can make your own user define litteral to make unsigned chars like

inline constexpr unsigned char operator ""_uc( unsigned long long arg ) noexcept 
{ 
    return static_cast< unsigned char >( arg ); 
}
//...
act(5_uc, 5_uc);

or you can just cast to them like

act((unsigned char)5, (unsigned char)5);

The actual wording that covers this case can be found in [dcl.init]/7

A narrowing conversion is an implicit conversion [...}

  • from an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted back to the original type [...]

emphasis mine

As you can see it requires the initizlizer to be a constant expression. Since function parameters are never constant expressions it doesn't matter how much static analysis the compiler does and how much proof it has. It is not a constant expression so it cannot be done.

Bonus question:

Since there is no literal suffix to get an unsigned char literal, how to workaround this bug || fix this non-standard code?

Just for fun, I propose you a way (not really practical) to pass your literal type values as arguments of the function (in a sense) avoiding the narrowing problem: you can receive the args values through a std::integral_constant

template <typename ... T, T ... args>
constexpr std::array<unsigned char, sizeof...(T)>
   act (std::integral_constant<T, args>...)
 {
   return std::array<unsigned char, sizeof...(T)>{args...};
 }

and call it as follows

act(std::integral_constant<int, 5>{}, 
    std::integral_constant<long long, 5ll>{});

This way, the args value are template values so the compiler is guaranteed that particular function instantiation is safe from the narrowing point of view (calling it with adequate values, obviously).

If you can use C++17, you obtain the same result (in a simpler and more practical way) simply passing the args as auto template values.

template <auto ... args>
constexpr auto act ()
 {
   return std::array<unsigned char, sizeof...(args)>{args...};
 }

// ...

act<5, 5ll>();
Related