1

Not sure if the following is possible in C++20 after a long and exhaustive search but asking anyway:

Using C++20 instead of C's va_list, can I: (1) Encode arbitrary argument lists passed to a variadic function template into a single variable so that (2) this variable can be passed to a different, non-templated function where it is (3) decoded and ultimately consumed by a printf()-like function?

That is, I would like to be able to write something similar to:

/*
 * main.cpp
 */
#include <very_old_library.h>
#include <utility>

typedef  /*** what goes here? ***/  AmazingVariadicArgumentEncoderType;

/* Impl26() is NOT templated and accepts a FIXED number of arguments */
void Impl26(const char *fmt, AmazingVariadicArgumentEncoderType encodedArgs)
{
    /*
     *  Decode encodedArgs here and represent them as decodedArgs (maybe?)
     */
    
    VeryOldLibraryFunctionWithPrintfSemantics(fmt, decodedArgs... /* not sure what syntax to use for decodedArgs here */);
}

template <typename... Args>
void Wrapper(const char *fmt, Args&&... args)
{
    AmazingVariadicArgumentEncoderType encodedArgs;
    
    encodedArgs = 
    /*
     *  Encode args here and represent them as encodedArgs.
     *
     *  Possibly using:
     *      std::forward<Args>(args) ...
     *  ?
     */
    
    Impl26(fmt, encodedArgs);
}

int main(int argc, char *argv[])
{
    // Each of these is a valid invocation of Wrapper().
    Wrapper(kOpaqueSecretString1, -123);
    Wrapper(kOpaqueSecretString2, "jenny", 8675309, 'q', argv[1]);
    Wrapper(kOpaqueSecretString3);
    
    return 0;
}


/*
 * very_old_library.h
 */
const char *kOpaqueSecretString1;
const char *kOpaqueSecretString2;
const char *kOpaqueSecretString3;
void VeryOldLibraryFunctionWithPrintfSemantics(const char *fmt, ...);

(The above code is, of course, a distilled version of the real problem I'm trying to solve.)

I'm struggling to find a suitable definition for AmazingVariadicArgumentEncoderType and the steps to encode to / decode from it.

Restating my question for clarity (thanks @Nelfeal):

Q: How do I write a non-template function that accepts a single parameter and passes it to something like printf() as multiple arguments?


Here are things which I expect people will suggest which will not work for me:

  1. Use std::format and/or std::iostream/std::cout

    I'm not actually printing to stdout. My code specifically interfaces with a library that exposes a printf()-like API which uses C-style var args and that library can't be replaced.

  2. Make Impl26() a function template.

    In my real code, Impl26() is a virtual member function and therefore cannot be templated.

  3. #include <stdarg.h>.

    I know I could use old C-style varargs in my new code, too, but I'd like to see whether it's possible to avoid doing so. This is as much an experiment and a learning exercise as it is a real problem I'm trying to solve.

  4. Encode as a std::tuple.

    I can't think of a way to pass a tuple of arbitrary variables of arbitrary types without making Impl26() a function template.

  5. Re-implement the format string parsing logic of printf()-like functions.

    I would like a direct pass-through and not try to guess the implementation details of the underlying library.

pion
  • 190
  • 1
  • 8
  • It's a little unclear to me where the actual question is. Do you just want to write a non-template function that accepts a single parameter and passes it to something like `printf` as multiple arguments? – Nelfeal Jul 26 '23 at 10:14
  • @Nelfeal yes, exactly – pion Jul 26 '23 at 10:15
  • 1
    I don't think that's possible unless you want to reimplement the "type decoding" part of the printf-like function (so checking `fmt` for `%d` and stuff). – Nelfeal Jul 26 '23 at 10:23
  • @Nelfeal Thanks! I've seen that approach, too, and forgot to add it to my 'unwanted solutions list. I've added it now. – pion Jul 26 '23 at 10:26
  • Whatever `AmazingVariadicArgumentEncoderType` is, it must lose all type information about the parameter pack. At this point, it is equivalent to a `va_list`, which you can probably pass to the appropriate function (like [`vprintf`](https://en.cppreference.com/w/c/io/vfprintf)), but that's just point 3 of your list. – Nelfeal Jul 26 '23 at 10:36
  • I am unsure of your question, but I have the impression that the answer is **yes**, as I implemented something like this. Here is a synopsis of what I wrote. Please tell me if this is what you are looking for, I might write an answer from that. – prapin Jul 26 '23 at 10:38
  • `template inline auto toPOD(T&& v) { ... } template static constexpr String::Type typeID(const T&) {...} class String { public: enum Type : unsigned char { kTermination, kTypeBool, kTypeChar, kTypeInt, ... }; template void appendFormat(const char* format, Args&&... args) { static const Type types[sizeof...(Args) + 1] = { typeID(args)..., kTermination }; appendFormat(types, format, toPOD(std::forward(args))...); } private: void appendFormat(const Type* types, const char* format, ...); };` – prapin Jul 26 '23 at 10:41
  • @prapin Thanks. I can't tell 100% but it looks like your version might be explicitly naming individual POD typenames with the intent to encode each one separately. For my purposes I want to keep the solution completely agnostic to whatever type of variable is passed and see if there is a general-purpose parameter packing solution. Would yours offer that? – pion Jul 26 '23 at 10:56
  • The answer would be simpler if the "arbitrary" arguments were constrained to a particular list of possible types. Then you can do "type decoding" without having to reimplement all the format string parsing logic. A similar but type-agnostic solution will necessarily use RTTI (`typeid`) or ask the caller to help with the type encoding. – Nelfeal Jul 26 '23 at 10:58
  • @Nelfeal I'm not sure I've seen an RTTI or caller-assisted solution and I'm having trouble visualizing either. Would you be open to sharing some sample code or a link? – pion Jul 26 '23 at 11:01
  • Well, the caller-assisted solution would be similar to prapin's but with the caller potentially providing more `typeID` specializations (more like overloads, really). As for the RTTI version, I actually don't know how it would work but I don't know that it's impossible either. – Nelfeal Jul 26 '23 at 11:09
  • That said, I don't know why you absolutely want a type-agnostic solution. The printf-like function you want to call necessarily has a finite set of types it can handle. `printf` for instance only has so many format specifiers available. – Nelfeal Jul 26 '23 at 11:11
  • A variadic function **cannot** be generically wrapped. Not in C and not in C++, not for love and not for money. If you doubt it, try writing a generic wrapper around the plain old `printf`. You cannot. That's the reason why `vprintf` and friends are exposed. If your library doesn't expose am equivalent v- interface, it is, shall we say, not a pinnacle of C coding. – n. m. could be an AI Jul 26 '23 at 11:12
  • @n.m.willseey'allonReddit [There are at least two ways to generically wrap a variadic function.](https://stackoverflow.com/questions/41400/how-can-i-wrap-a-function-with-variable-length-arguments) Whether that's useful or not is another question. – Nelfeal Jul 26 '23 at 11:22
  • @pion The `toPOD` template function is an identity function (just returning its argument) for most used types: `bool`, integers, floating points, pointers. For some often used structures that we want to format easily like a `Point` , `toPOD` might do some conversion like calling `toString()`. My first versions of `appendFormat` didn't use `toPOD` at all, it is not a mandatory part. In particular, if the goal is to replace *existing* `printf`, we are sure arguments are already valid POD. – prapin Jul 26 '23 at 12:00
  • does the target API expose a `vprintf`? otherwise you're going to have pain on this. In theory you could use a [fold expression](https://en.cppreference.com/w/cpp/language/fold) to unpack the args and not really care about the types per se. But I'm not actually sure it would work? – Mgetz Jul 26 '23 at 12:10
  • Two clarifications (1) I mean wrapped with a function (you can still wrap with a function template or a macro for example). (2) Clients that need to call a variadic function cannot delegate this operation to a generic wrapper. They still need to call a variadic function themselves. The call may be delayed (with a lambda etc) and it may be to a function passed as a parameter to the lambda, but it needs to be done there. So if the goal is to get rid of C-style variadic functions everywhere except in the wrapper, then it's not possible. – n. m. could be an AI Jul 26 '23 at 14:54

1 Answers1

4

Use std::function to make the call to VeryOldLibraryFunctionWithPrintfSemantics in a context where the template arguments are known:

/*
 * main.cpp
 */
#include <utility>
#include <functional>

using EncoderCallback = void(*)(char const*, ...);

using AmazingVariadicArgumentEncoderType = std::function<void(EncoderCallback, char const*)>;

void Impl26(const char *fmt, AmazingVariadicArgumentEncoderType encodedArgs) {
    encodedArgs(VeryOldLibraryFunctionWithPrintfSemantics, fmt);
}

template <typename... Args>
void Wrapper(const char *fmt, Args&&... args) {
    AmazingVariadicArgumentEncoderType encodedArgs = [=](EncoderCallback callback, char const* fmt) {
        callback(fmt, args...);
    };
    Impl26(fmt, encodedArgs);
}

int main(int argc, char *argv[])
{
    // Each of these is a valid invocation of Wrapper().
    Wrapper(kOpaqueSecretString1, -123);
    Wrapper(kOpaqueSecretString2, "jenny", 8675309, 'q', argv[1]);
    Wrapper(kOpaqueSecretString3);
    
    return 0;
}
chrysante
  • 2,328
  • 4
  • 24