Is it possible to pass an unscoped enum to va_start?

Viewed 102

The Problem

I have a legacy codebase with something like this:

enum MyEnum { Foo, Bar, Baz };

void someFunc(MyEnum enumVal, ...)
{
    va_list args;
    va_start(args, enumVal); 
    // do something with args...
}

I cannot easily change MyEnum to a scoped enum, since it's used throughout our whole code base (some of which I don't even have access to). Compiling this code (with '-Wall -std=c++17'), Clang 14 emits this warning:

<source>:8:20: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs]
    va_start(args, enumVal); 

As far as I can tell, the problem here is a combination of these three rules (or lack thereof, see the third point) in the C++ (17, but should be the same for 11 or 14) standard:

1. Unscoped enums are (in a function call) always promoted to their underlying type.

The Integer Promotion rules [conv.prom] say:

  1. A prvalue of an unscoped enumeration type whose underlying type is not fixed (7.2) can be converted to a prvalue of the first of the following types that can represent all the values of the enumeration: […]

  2. A prvalue of an unscoped enumeration type whose underlying type is fixed (7.2) can be converted to a prvalue of its underlying type. […]

While these rules only state that there can be an integer conversion, [expr.call]/7 says:

If the argument has integral or enumeration type that is subject to the integral promotions […], the value of the argument is converted to the promoted type before the call.

So basically, any enum (that is not an enum class) must be promoted to its underlying type before call.

2. va_start must take an argument that is 'compatible' to the resulting type

The rules for va_start in [cstdarg.syn]/1 state:

If the parameter parmN is […] of a type that is not compatible with the type that results when passing an argument for which there is no parameter, the behavior is undefined.

Here I'm not sure what the type that results when passing an argument for which there is no parameter means. I guess that it's the (promoted!) type of the parmN parameter when you specify at least one of the variadic parameters. Not sure why that influences the type of the parmN parameter, but basically I guess it means a call like

someFunc(MyEnum::Foo, 42);

In this case, 42 is the argument for which there is no parameter.

In any case, the type of the parameter passed to va_start is MyEnum, and the (promoted) type of the argument is some integer type. Leading to the third point:

3. It is unclear what 'compatible' means in this context

While the C standard clearly states which types are 'compatible' (see 6.2.7 Compatible type and composite type of the C standard), the C++17 standard explicitly states in its list of differences to C, in [diff.basic]/6.9:

Change: C allows “compatible types” in several places, C++ does not.

And, consequently, does not give a specification of what 'compatible types' are. So to me, it's not clear how the 'compatible' requirement in [support.runtime]/3 is to be interpreted. If we apply the old C-standard rules of compatibility, I guess that enums are only ever compatible to enums, not to ints.

If that is a correct interpretation, I see no way of using an unscoped enum parameter in va_start.

The Question

Is there, in C++17, a non-undefined-behavior way of passing an unscoped enum parameter of a function to va_start? Or, phrased differently: Given a variadic function which has an unscoped enum parameter as its last named parameter (before the ...), is there a way of retrieving the va_list?

0 Answers
Related