5

Is the following usage of a lambda wrong, fragile, or silly? It works on VC++ 2012, but I am concerned that there are some variable-argument/lambda stack interactions that make this dangerous.

class
ArgumentException : public std::runtime_error
{
public:
   ArgumentException( 
      const char* format_,
      ... )
      : std::runtime_error(
         [&]()
         { 
            char buffer[2048];
            va_list arguments;
            va_start ( arguments, format_ );  
            int writtenCount = vsnprintf( buffer, 2048, format_, arguments );
            va_end ( arguments );
            return std::string(buffer); 
         }() )
   {
   }
};
Elpoca
  • 65
  • 5
  • 2
    The most obviously wrong thing here is that you're returning a pointer to the local variable `buffer`. As for `va_list` from a lambda with a captured argument, that's an interesting question. I know a few good ways to make something like that happen, but yours isn't one of them. – zneak Apr 30 '14 at 16:43
  • good question... but I don't think it's official in C++... As far as I know, `va_list`'s are just there for C codes while C++ discourage the use of them. But I'm not sure... – Wagner Patriota Apr 30 '14 at 16:52
  • You are probably better off using a variadic template. – kec Apr 30 '14 at 17:06

3 Answers3

4

The C++11 standard explicitly makes va_list and the supporting macros available through <cstdarg> (§18.10, table 37) but makes no attempt to restrict their use or (re)define their meaning, so I'm going to turn to the C standard.

§7.15.1.4 says that:

void va_start(va_list ap, parmN );

The parameter parmN is the identifier of the rightmost parameter in the variable parameter list in the function definition (the one just before the , ...). If the parameter parmN is declared with the register storage class, with a function or array type, or with a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefined.

In your case, format_ is not an argument (it's a captured variable in your lambda) and the function in which va_start is called is not even the one with the variable parameter list, so you're arguably very much in the realm of undefined behavior. Not to mention that the argument promotion rules of the C language can't deal with reference types, and therefore it can't correctly deal with the fact that format_ is a reference, not a straight-up pointer.

As far as I know, it's syntactically infeasible to use variadic parameters in a constructor's initializer list. (See this guy below who did it.) You could, however, use variadic templates to forward the parameters to a "clean" C-style variadic parameters function:

#include <cstdarg>
#include <cstdio>
#include <string>

std::string stringprintf(const char* format, ...)
{
    char buffer[0x2000];
    va_list ap;
    va_start(ap, format);
    vsnprintf(buffer, sizeof buffer, format, ap);
    va_end(ap);
    return buffer;
}

class ArgumentException : public std::runtime_error
{
public:
    template<typename... T>
    ArgumentException(const char* format, T... arguments)
    : std::runtime_error(stringprintf(format, arguments...))
    { }
};

Also consider using <stdexcept>'s invalid_argument exception subclass.

Community
  • 1
  • 1
zneak
  • 134,922
  • 42
  • 253
  • 328
  • +1 the latter work-around was nearly verbatim the same solution [i had as well](http://ideone.com/TI8WBJ). nice answer. – WhozCraig Apr 30 '14 at 17:21
  • Does it actually work with `T&&`? I thought that it would send references to the variadic function and blow it up. – zneak Apr 30 '14 at 17:25
  • *"In your case, format_ is not an argument (it's a captured variable in your lambda)"* I'm not sure about that. Capturing by reference is weird IMO, since it has been specified to allow reusing the stack frame of the surrounding scope. I.e., AFAIK, the name `format_` does refer directly to the parameter of the constructor. For capture-by-copy, there's a transformation of this access to the data member in the closure, but not for capture-by-ref. – dyp Apr 30 '14 at 17:25
  • @zneak Yes it does work, and with or without references you can't send a non-trivial type to a variadic C argument list (my clang spanks me for even trying). Its an interesting point though. I'll have to brain-feed that for awhile. Fun question. – WhozCraig Apr 30 '14 at 17:31
  • 2
    @dyp, I didn't know that. I think that the phrasing is still mostly correct though: in this context, the called function is the lambda, not the constructor, so `format_` is not an argument to the lambda. – zneak Apr 30 '14 at 17:33
  • I agree with that: It's not an argument parameter of the lambda and therefore probably UB. – dyp Apr 30 '14 at 17:38
  • @dyp: FWIW, I got an error with g++ 4.8.2 with the original version for trying to call va_start in the lambda. – kec Apr 30 '14 at 17:39
  • 1
    @WhozCraig indeed this is covered in section `5.2.2 p7`, I quote that paragraph [here](http://stackoverflow.com/questions/1657883/variable-number-of-arguments-in-c/16338804#16338804). – Shafik Yaghmour Apr 30 '14 at 17:44
  • Using variadic templates is a nice, proper way of accomplishing what I want to do, but sadly Visual C++ 2012 does not support them (at least, not the 2012 version I need to support). – Elpoca Apr 30 '14 at 18:29
  • @Elpoca, you could also do `throw ArgumentException(stringprintf(...))` then. – zneak Apr 30 '14 at 18:30
  • @zneak I know, and I could have done this to begin with, but I was looking for a more encapsulated approach. – Elpoca Apr 30 '14 at 18:34
2

I don't recommend doing this, so this is more just an exercise in whether or not it is possible to write a conforming implementation without a variadic template. I think this is, though it's not elegant or pretty:

#include <stdexcept>
#include <cstdarg>

std::string
helper(const char *fmt, va_list args) {
    char buffer[2048];
    int writtenCount = vsnprintf(buffer, sizeof buffer, fmt, args);
    return std::string(buffer);
}

class ArgumentException : public std::runtime_error {
    public:
       ArgumentException(const char* fmt, ...)
           : std::runtime_error(helper(fmt, (va_start(args, fmt), args))) {
           va_end(args);
       }
    private:
        va_list args;
};

By the way, the original attempt gave me an error in g++, claiming that I was using va_start in a function with fixed arguments, which is correct, I believe.

kec
  • 2,099
  • 11
  • 17
  • You proved me wrong. Don't forget to call `va_end`, though. – zneak Apr 30 '14 at 17:25
  • It's called in `helper`. – kec Apr 30 '14 at 17:29
  • Since we're having so much fun with that, here's to confuse the compiler: http://ideone.com/s6IGpD (also note that the output is blank) – zneak Apr 30 '14 at 17:44
  • That's just cruel. FWIW, g++ 4.8.2 gave me a warning on it. clang++ did not. Both version gave wrong output. – kec Apr 30 '14 at 18:03
  • Clang++ refuses to compile lambdas with variadic parameters iff the parent function does't have variadic parameters, I've just filed a bug report for it. – zneak Apr 30 '14 at 18:24
  • I ended up using this, but I feel... dirty... about it. And I think it relies upon the va_start macro behaving nicely. – Elpoca Apr 30 '14 at 18:37
  • @Elpoca: Ugh. I think it's standard compliant, though. At least from my reading of the C99 stdarg section. – kec Apr 30 '14 at 20:40
  • 1
    `va_end` must be called in the same function as where `va_start` is called (as stated by the man page). They are not functions but macro and use the stack to work (which changes in a different function). It might work on your platform because va_end does nothing on x86/amd64 but it's not always the case (and new GCC tends to use builtin here to check that the parameter count match the number times va_arg is called). Thus this code is wrong and should be fixed. – xryl669 Dec 29 '18 at 22:31
  • @xryl669: Good point. I don't quite remember the purpose of this, but I think I fixed it in a reasonable way. – kec Apr 17 '19 at 00:14
0

For posterity's sake, adding WhozCraig's solution here:

class ArgumentException : public std::runtime_error
{
private:
    static std::string mkmsg(const char *fmt, ...)
    {
        char buffer[2048];
        va_list args;
        va_start ( args, fmt );
        int writtenCount = vsnprintf( buffer, 2048, fmt, args );
        va_end ( args );
        if (writtenCount<=0)
            *buffer = 0;
        return buffer;
    }

public:
    template<class... Args>
    ArgumentException(const char* fmt, Args&&... args )
        : std::runtime_error( mkmsg(fmt, std::forward<Args>(args)... ) )
    {
    }
};
Elpoca
  • 65
  • 5