How can C++ and C variadic arguments be used together?
Asked Answered
B

2

14

Generally, using the C++11 variadic template feature with functions requires the variadic-based function arguments to be the last in the function argument list. There is one exception; they are the next-to-last arguments if there are C-level variadic arguments, which must be dead last.

template < typename ...Args >
int  super_printf( Something x, Args &&...a, ... );

I sometimes randomly think about C++, and I wondered how such a function can be implemented. I first thought about the usual recursive peeling of arguments from a, then I remembered that the C-level varargs don't cascade. I have to turn them to a definitive va_list right away.

template < typename ...Args >
int  super_vaprintf( Something x, std::va_list &aa, Args &&...a );
// Note that "aa" is passed by reference.

template < typename ...Args >
int  super_printf( Something x, Args &&...a, ... )
{
    std::va_list  args2;
    int           result;

    va_start( args2, XXX );  // (A)
    try {
        result = super_vaprintf( x, args2, std::forward<Args>(a)... );
    } catch ( ... ) {
        va_end( args2 );  // (1)
        throw;
    }
    va_end( args2 );  // (2)
    return result;

    // Can (1) and (2) be compacted with RAII using a custom deleter lambda
    // in std::unique_ptr or something?  Remember that "va_end" is a macro!
}

The usual C++ variadic recursive peeling happens in the super_vaprintf call. At line (A), what goes in the place of XXX, "a" or "a..."? What happens if a is empty, does x go there instead? If that last question's true, are we screwed if there's no x; that there's no arguments besides the variadic ones? (And if it's true, how do we conditionalize the code to use x when a is empty, and a otherwise?)

...

I just looked at my copy of the C++11 standard for any assistance here. There doesn't seem to be any. This would prompt a request for the C++ committee to come back to fix this, but I'm not sure that there's any way such a function could be called without the C++ varargs taking everything. Am I wrong; can a function call be made to use both C++ and C varargs? Or is mixing only useful for declarations, in terms of Stupid (Template) Instantiation Tricks?

Biologist answered 15/5, 2013 at 11:27 Comment(7)
Just after posting this, I realized that maybe something like "super_printf<int, int>( Something{}, 1, 2, 3, 4 )" could force some arguments as C-level (the "3" and "4" in this case).Biologist
they are orthogonal, since c va works at run-time, and c++ variadic templates work at compile timeRealtor
@CTMacUser: yes, that would work.the question still remains how it is useful, and I would say in metaprogramming it might be. maybe you can find more insight in reading the variadic template proposals, they might tell why this is possible.Bashemath
Obligatory link to the sextuple dot (double ellipsis)Eliathas
@Cubbi, the usage example by @Potatoswatter is interesting. You can channel C++ varargs as C varargs, but I'm kind-of going the other way.Biologist
@zahir, if you want me to watch an hour-long video, it'll be nice to specify which part. If you meant his checked-printf example, my query is going in the other direction. If you meant his outlook on variadics, I'm querying on legality/"could", not morality/"should".Biologist
@zahir, I've already watched the whole thing. As I said before, my code is translating in the other direction; and if you don't approve of that direction, I'm still arguing that it should be legal, regardless if it should be moral.Biologist
H
5

When you call a function whose last parameter is a pack, all the arguments become part of that pack. There is nothing left for va_args. Your usage of explicit template arguments is misleading because they are not exclusive; they simply precede implicit arguments.

To defeat deduction, you need a reference:

(& super_printf<int, int>) ( 0L, 1, 2, 3, 4, 5 )

This is fairly contrived, but now you have the problem of nothing to pass to va_start.

To provide a reasonable interface to users, just add a parameter between the two lists.

struct va_separator {}; // Empty; ABI may elide allocation.

template < typename ...Args >
int  super_printf( Something x, Args &&...a, va_separator, ... );

This super_printf will need both explicit arguments to define the pack and an explicit separator argument. But you can alternatively provide a public function which receives all its arguments by pack, then finds the separator and forwards to super_printf using an explicit argument list comprising the pack elements before the separator.

Hautegaronne answered 29/10, 2013 at 4:47 Comment(1)
MSVC 2019 fails to defeat deduction with reference. See godbolt.org/z/G9W8cx6vr.Gibeonite
B
0

I've tried this code out on a compiling web site (Coliru), with GCC 4.8, and the results look bleak. I don't know if it's GCC in particular, or if all the other compilers out there do something similar. So can people with other compilers (Clang, Visual C++, Intel, etc.) try this out?

#include <cstdarg>
#include <iostream>
#include <ostream>
#include <utility>

template < typename ...Args >
int  super_vaprintf( long, std::va_list &, Args &&... )
{
    return 17;
}

template < typename ...Args >
int  super_printf( long x, Args &&...a, ... )
{
    std::va_list  args2;
    int           result;

    va_start( args2, a );  // (A)
    try {
        result = super_vaprintf( x, args2, std::forward<Args>(a)... );
    } catch ( ... ) {
        va_end( args2 );
        throw;
    }
    va_end( args2 );
    return result;
}

int main() {
    std::cout << super_printf<int, int>( 0L, 1, 2, 3, 4, 5 ) << std::endl;  // (B)
    return 0;
}

The call to super_printf on line (B) explicitly sets the C++ varargs to two int entries. This will make the function use arguments 1 and 2 as C++ varargs and the latter three as C varargs.

On line (A), the compiler insists that the code with a in it have a "..." somewhere. So I change it to:

va_start( args2, a... );  // (A)

I get another error about having the wrong number of arguments. It makes sense since a expands to two arguments. If I change line (B) to one C++ vararg:

std::cout << super_printf<int>( 0L, 1, 2, 3, 4, 5 ) << std::endl;  // (B)

it works just fine. If I remove the C++ varargs entirely:

std::cout << super_printf<>( 0L, 1, 2, 3, 4, 5 ) << std::endl;  // (B)

we get the wrong-number-or-arguments error again, because a has length of zero). If we do this when a is empty:

va_start( args2, x /*a...*/ );  // (A)

the code works again, although there is a warning about x not being the last named parameter.

We can approach the example in another way. Let's reset to:

va_start( args2, a... );  // (A)
//...
std::cout << super_printf( 0L, 1, 2, 3, 4, 5 ) << std::endl;  // (B)

where all the arguments after the first are grouped as C++ varargs. We get the same too-many-arguments error in va_start, of course. I progressively comment out trailing arguments. It works when there are exactly two arguments left (which makes a have exactly one argument).

There is also an error when there's only one argument left, but the error message changes to explicitly say "too few" arguments instead of "wrong amount." Like before, I switched out "a..." for "x" in line (A), and the code was accepted, but there was no warning. So it seems that when I explicitly include "<Whatever>" for super_printf in line (B) I get a different parser error path than when I don't include them, although both paths go to the same conclusion.

Time to tell the committee that they overlooked something....

Biologist answered 16/5, 2013 at 9:17 Comment(3)
Regarding your final paragraph... no, not really.Thesaurus
@LightnessRacesinOrbit, actually yes.Biologist
:P BTW Coliru supports ClangThesaurus

© 2022 - 2024 — McMap. All rights reserved.