[Disclaimer: I know an answer to this question. I thought it might be of some general interest.]
Question: How can we have a type trait that produces the type that results from performing default argument promotions?
Motivation: I would like to be able to use variable arguments portably. For example:
void foo(char const * fmt, ...); // Please pass: * unsigned short
// * bool
// * char32_t
// * unsigned char
When passing arguments to a function call without parameters, i.e. matching the ellipsis, the arguments undergo default argument promotion. So far so good, but those promotions are platform dependent. I can recover the arguments with va_arg(ap, T)
, but what is T
?
Now, for some simple situations this is easy: For example, I can always say:
unsigned short n = va_args(ap, unsigned int);
The default promotion will result in either a signed int
or an unsigned int
, but according to, say, C11 7.16.1.1/3, va-casting to unsigned int
is always fine, since even if the default promotion results in an int
, the original value can be represented by both types.
But what type should I cast to when I expect a char32_t
? C++11 4.5/2 leaves the resulting type wide open. So I would like a trait that lets me write:
char32_t c = va_args(ap, default_promote<char32_t>::type);
How to do this?
Bonus points for a trait that produces a static assertion when the parameter type must not be passed as a variable argument.
Here's a skeleton of a solution that works for "most" types (integral, float, unscoped enumeration, arrays, pointers, pointers-to-member, functions, function pointers).
#include <type_traits>
template <typename U>
struct default_promote
{
// Support trait for scoped enums
template <typename E, bool IsEnum>
struct is_unscoped_enum : std::false_type { };
template <typename E> struct is_unscoped_enum<E, true>
: std::is_convertible<E, typename std::underlying_type<E>::type> { };
// Floating point promotion
static double test(float);
// Integral promotions (includes pointers, arrays and functions)
template <typename T, typename = typename std::enable_if<!is_unscoped_enum<T, std::is_enum<T>::value>::value>::type>
static auto test(T) -> decltype(+ std::declval<T>());
template <typename T, typename = typename std::enable_if<is_unscoped_enum<T, std::is_enum<T>::value>::value>::type>
static auto test(T) -> decltype(+ std::declval<typename std::underlying_type<T>::type>());
// Pointers-to-member (no promotion)
template <typename T, typename S>
static auto test(S T::*) -> S T::*;
using type = decltype(test(std::declval<U>()));
};
It does not provide diagnostics for types that cannot safely be passed through an ellipsis. Also, this solution subsumes the decay that types undergo when passed as variable function arguments, so it is not strictly about promotion only.
It works by handling explicitly the pointer-to-member types and the floating point conversion, and by relying on the unary operator +
for integral and unscoped enumeration types; e.g. C++11 5.3.1/7:
The operand of the unary
+
operator shall have arithmetic, unscoped enumeration, or pointer type and the result is the value of the argument. Integral promotion is performed on integral or enumeration operands. The type of the result is the type of the promoted operand.
Some extra work is needed to handle enumerations, since it is possible to overload operators for enumerations (both scoped and unscoped), and so the naive unary plus operator must be used with care. That is, we must consider the promotion of the underlying type when the enum is unscoped, and forbid scoped enums entirely.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With