将可变参数转换为lambda表达式

5
以下使用lambda表达式的方式是否错误、脆弱或愚蠢?它在VC++ 2012上运行正常,但我担心有一些变量参数/lambda栈交互会使其变得危险。
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); 
         }() )
   {
   }
};

2
这里最明显的错误是你返回了一个指向局部变量 buffer 的指针。至于在具有捕获参数的 lambda 函数中使用 va_list,那是一个有趣的问题。我知道一些好的方法可以实现类似的效果,但你的方法不是其中之一。 - zneak
好问题...但我不认为它在C++中是官方的...据我所知,va_list只是为C代码而存在,而C++则不鼓励使用它们。但我不确定... - Wagner Patriota
你最好使用可变参数模板。 - kec
3个回答

4
C++11标准明确通过(§18.10,表37)提供了va_list和支持宏,但没有限制它们的使用或重新定义它们的含义,因此我将转向C标准。 §7.15.1.4表示:参数parmN是函数定义中可变参数列表中最右侧参数的标识符(就在, ...之前的那个参数)。如果参数parmN声明为register存储类、函数或数组类型或与应用默认参数提升后的类型不兼容,则行为未定义。
在您的情况下,format_不是一个参数(它是lambda中捕获的变量),调用va_start的函数甚至不是具有可变参数列表的函数,因此您可以说非常处于未定义行为的领域。更不用说C语言的参数提升规则无法处理引用类型,因此它无法正确处理format_是引用而不是纯指针的事实。 据我所知,在构造函数的初始化列表中使用可变参数是语法上不可行的。看看下面这个人是如何做到的。)但是,您可以使用可变模板将参数转发到“干净”的C风格可变参数函数:
#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...))
    { }
};

此外,还应考虑使用<stdexcept>invalid_argument异常子类。

它是否可以使用 T&& 正常工作?我认为它会将对可变参数函数的引用发送出去并使其炸裂。 - zneak
@zneak 是的,它确实可以工作,无论是否使用引用,您都无法将非平凡类型发送到可变参数C参数列表(我的clang会惩罚我甚至尝试)。这是一个有趣的观点。我需要一段时间来消化它。有趣的问题。 - WhozCraig
2
@dyp,我不知道这一点。但我认为措辞仍然大体正确:在这种情况下,被调用的函数是lambda而不是构造函数,因此format_不是lambda的参数。 - zneak
我同意这个观点:这不是lambda的一个参数,因此可能会导致未定义行为(UB)。 - dyp
1
@WhozCraig 的确,这在第 5.2.2 p7 节中有涉及,我在此引用该段落 here - Shafik Yaghmour
显示剩余6条评论

2

我不建议这样做,所以这只是一个练习,目的是测试是否可以在没有可变参数模板的情况下编写符合规范的实现。虽然我认为这是可能的,但并不优雅或美观:

#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;
};

顺便提一下,最初的尝试在g++中给我报错,声称我在带有固定参数的函数中使用了va_start,我认为这是正确的。


你证明了我是错的。不过别忘了调用 va_end - zneak
这太残忍了。对于 g++ 4.8.2 给出了一个警告。clang++ 没有。两个版本都给出了错误的输出。 - kec
我最终使用了这个,但是我感觉...很不舒服...而且我认为它依赖于va_start宏的良好行为。 - Elpoca
1
va_end 必须在与 va_start 调用相同的函数中调用(如 man 手册所述)。它们不是函数,而是宏,并使用堆栈来工作(在不同的函数中会发生变化)。它可能在您的平台上运行,因为 va_end 在 x86/amd64 上什么也不做,但并非总是如此(新的 GCC 倾向于在此处使用内置函数来检查参数计数是否匹配 va_arg 被调用的次数)。因此,此代码是错误的,应进行修正。 - xryl669
@xryl669:好观点。我不太记得这个的目的,但我认为我以一种合理的方式修复了它。 - kec
显示剩余4条评论

0

为了后人的纪念,这里添加WhozCraig的解决方案:

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)... ) )
    {
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接