定义一个只接受字符串字面值的函数 - 以便传递给宏。

3
我正在编写一个使用C库的C++程序。这个C库有一个日志系统,其核心是一个类似于下面的宏定义:
#define MACRO(s, ...) printf("log: " s "\n", ##__VA_ARGS__)

// supposed to be called like:
MACRO("network error %d", errno)

如你所见,宏的第一个参数只能是字符串字面量,因为展开时会将这两个字面量拼接在一起。以下的写法是不可行的:
char msg[] = "network error %d";
MACRO(msg, errno); // syntax error!

我想写一个函数来封装一些重复的工作(检查错误代码,如果有错误,则记录并抛出)。但是我无法得到比下面更好的东西:
#include <stdexcept>
#include <cstdarg>
#include <cstdio>

void throw_on_err(err_t err, const char *format...) {
  if (err != OK) {
    char buffer[128];
    va_list args;
    va_start(args, format);
    vsnprintf(buffer, sizeof(buffer), format, args);
    va_end(args);
    MACRO("%s", buffer);
    throw std::runtime_error(buffer);
  }
}

// supposed to be called like:
throw_on_err(errno, "network error");

但是这有一些问题:它人为地限制了错误消息的大小,并且(vsn)printf的工作被执行了两次。 现在,我知道我可以用宏替换throw_on_err,但我想避免这种解决方案:它更难调试,类型安全性较差。我想使用库的宏,因为它是标准的,并且比我展示的内容做更多的工作(这些工作与手头的问题无关,比如给输出添加颜色、检查日志级别等)。我尝试过使用const char*参数、constexpr函数来操作,但没有成功。

你已经在使用可变参数了,所以就类型安全而言,你已经深入其中了。 - ph3rin
那个 char buffer[128] 简直就是在寻找缓冲区溢出的问题... 将它改为至少 char buffer[1000]。你在节省栈内存方面并不会获得太多好处。 - selbie
1
你不能通过函数传递一个字符串字面量,宏是唯一的方法。或者可以放弃 C 宏,在你的函数中编写等效的代码。 - Alan Birtles
@selbie vsnprintf不会溢出。我在开发嵌入式系统。我没有额外的1000字节堆栈可供使用。 - N. I.
3个回答

3
诚实地说,我只会这样做:
void throw_on_err(int err, const std::string& message)
{
    if (err != 0)
    {
        MACRO("%s", message.c_str());
        throw std::runtime_error(message);
    }
}

这很容易促进您的基本案例:

throw_on_err(errno, "network error");

而且,如果有人想通过内联字符串连接将额外的参数插入其中,它也可以正常工作。
const char* reason = getLastError();
throw_on_err(errno, std::string("network error: ") + reason);

如果你需要在日志函数中使用可变参数,考虑不再使用 printf 风格的可变参数和前导格式字符串,而是只使用一个在头文件中声明的可变模板函数。

#include <sstream>
#include <string>

template <typename T>
void throw_on_err_impl(std::ostringstream& ss, int argIndex, const T& t)
{
    if (argIndex > 0)
    {
        ss << " ";
    }
    ss << t;
    MACRO("%s", ss.str().c_str());
    throw std::runtime_error(ss.str());
}

template<typename T, typename... Rest>
void throw_on_err_impl(std::ostringstream& ss, int argIndex, const T& t, Rest... rest)
{
    if (argIndex > 0)
    {
        ss << " ";
    }
    ss << t;
    throw_on_err_impl(ss, argIndex+1, rest...);
}


template<typename... Args>
void throw_on_err(int errorCode, Args... args)
{ 
    if (errorCode == 0)
    {
        return;
    }
    std::ostringstream ss;
    throw_on_err_impl(ss, 0, args...);
}

// this single parameter overload is optional.
inline void throw_on_err(int errorCode)
{
    throw_on_err(errorCode, "generic error");
}


然后可以以任何方式调用上述内容:
简单地如下:
throw_on_err(errno, "network error");

或者类似这样的东西:
std::string host = "www.stackoverflow.com";
std::string internalErrorReason = "tls handshake failure";
int internalStep = 9;
throw_on_err(errno, "network error:", internalErrorReason, "internal step:", internalStep);

1
在C++17中,使用折叠表达式,递归可以被替换为 std::ostringstream ss; const char* sep = ""; (((ss << sep << args), sep = " "), ...);(还可以处理空包)。 - Jarod42
最好使用MACRO("%s", std::move(ss).str().c_str());:在C++20中,可以使用优化的str()成员函数重载,在旧版本的C++中也不会有问题。 - fabian
通常情况下,我会传递一个 std::string_view,而不是要求构造一个 std::string - Toby Speight

0
你可以创建一个自定义的字符串类型,可以通过+运算符进行连接,并允许隐式转换为char const*,然后将这个类型传递给函数。这样你就可以将+arg+作为宏的参数传递。
但是这种方法有几个问题:它人为地限制了错误消息的大小,并且(vsn)printf的工作会被重复执行两次。
无论你对格式字符串有多少信息,都无法在编译时确定所需的缓冲区大小。原因是格式字符串可能包含%s,而相应的参数类型为char const*;在这种情况下,没有办法在不检查C风格字符串内容的情况下确定字符串长度。无论如何,你都需要为输出进行动态内存分配,无论你是否使用printf风格的功能。
我建议使用C++写入std::ostream
template<class T>
concept ostream_writable = requires(T t, std::ostream & s)
{
    {s << t};
};

class throw_on_err
{
    std::optional<std::ostringstream> m_errorReporter;
public:
    throw_on_err(int errorCode)
    {
        if (errorCode != 0)
        {
            m_errorReporter.emplace();
        }
    }

    /**
     * the destructor is responsible for throwing the exception
     */
    ~throw_on_err() noexcept(false)
    {
        if (m_errorReporter.has_value())
        {
            auto const message = std::move(m_errorReporter.value()).str();
            MACRO("%s", message.c_str());
            throw std::runtime_error(message);
        }
    }

    // helper functions for constructing the error step by step
    explicit operator bool() const noexcept
    {
        return m_errorReporter.has_value();
    }

    bool operator!() const noexcept
    {
        return !m_errorReporter.has_value();
    }

    // stream operator

    template<ostream_writable Arg>
    friend throw_on_err& operator<<(throw_on_err& s, Arg&& arg)
    {
        if (s.m_errorReporter.has_value())
        {
            s.m_errorReporter.value() << std::forward<Arg>(arg);
        }
        return s;
    }

    template<ostream_writable Arg>
    friend throw_on_err& operator<<(throw_on_err&& s, Arg&& arg)
    {
        if (s.m_errorReporter.has_value())
        {
            s.m_errorReporter.value() << std::forward<Arg>(arg);
        }
        return s;
    }
};

int main() {
    try
    {
        throw_on_err(1) << "network error";
    }
    catch (std::exception const& ex)
    {
        std::cerr << "exception caught: " << ex.what() << '\n';
    }

    try
    {
        throw_on_err(0) << "no error";
    }
    catch (std::exception const& ex)
    {
        std::cerr << "exception caught: " << ex.what() << '\n';
    }

    try
    {
        throw_on_err err(2);
        if (err)
        {
            err << "some other info we want to add: "
                << "foo bar";
        }
    }
    catch (std::exception const& ex)
    {
        std::cerr << "exception caught: " << ex.what() << '\n';
    }
}

顺便说一句:这是答案开头提到的解决方案的一个例子。正如之前提到的,你也无法满足所有要求,采用这种方法也是如此...

template<size_t N>
struct FixedSizeString
{
    char m_value[N];

    constexpr FixedSizeString()
        : m_value{}
    {}

    constexpr FixedSizeString(char const(&value)[N])
        : m_value{}
    {
        std::copy(value, value + N, m_value);
    }
    
    constexpr operator char const*() const
    {
        return m_value;
    }

};

template<size_t N, size_t M>
constexpr auto operator+(char const (&s1)[N], FixedSizeString<M> const& s2)
{
    FixedSizeString<N + M - 1> result;
    std::copy(
        s2.m_value,
        s2.m_value + M,
        std::copy(s1, s1 + (N - 1), result.m_value)
    );
    return result;
}

template<size_t N, size_t M>
constexpr auto operator+(FixedSizeString<N> const& s1, char const (&s2)[M])
{
    FixedSizeString<N + M - 1> result;
    std::copy(
        s2,
        s2 + M,
        std::copy(s1.m_value, s1.m_value + (N - 1), result.m_value)
    );
    return result;
}

#define MACRO(s, ...) printf("log: " s "\n", ##__VA_ARGS__)

template<size_t N, class...Args>
void f(FixedSizeString<N> const& format, Args...args)
{
    MACRO(+format+, args...);
}

template<size_t N, class...Args>
void f(char const (&format)[N], Args...args)
{
    MACRO(+FixedSizeString(format)+, args...);
}

int main()
{
    constexpr char const Format[] = "This is a message: \"%\"";
    f(Format, "Hello World");
}

-1
在C++中的一般准则是:
1. 避免使用预处理宏:
我们在C++中有模板、constexpr和内联构造等现代化特性。这些现代化的构造已经覆盖了大多数常规宏的使用场景。
2. 避免使用printf系列函数:
由于程序员的错误,printf系列函数容易导致许多运行时错误。在现代C++中,可以使用头文件中的std::format和std::format_to提供类型安全的替代方案,并具备编译时错误检测功能。
3. 避免使用C风格的可变参数函数:
在支持C++可变参数模板的情况下,支持C风格可变参数函数只是为了向后兼容而存在,并不适用于新代码。因此,在必要时请使用可变参数模板。
综上所述,我会这样写:
#include <format>
#include <concept>
#include <iostream>
#include <exception>
#include <string_view>
#include <system_error>

void throw_prompt(std::runtime _error const err){
    std::cerr << std::string_view{err.what();} << "\n";
    throw err;
};// ver1

void throw_prompt(int val, std::string_view msg){
    throw_prompt(
         std::format("{:d}:\t {:s}", val, msg)
    );
};// ver2

void throw_prompt(std::errc err){
    auto const std::error_condition ec{err};
    throw_prompt(ec.value(), ec.message());
}; //ver3

现在像这样使用它们:

// call ver3:
throw_prompt(std::errc::connection_aborted);
// call ver2:
throw_prompt(4000, std::format("custom err with {}, and {}", 1, 2));
// call ver1:
throw_prompt(std::format("custom err with {}, and {}", 3, 4));

可变参数模板版本似乎是不必要的定义,但它将使用std::format_string作为第一个参数。为了避免歧义问题,这个版本有一个不同的名称:
//variadic version:
template<typename ... targs>
void throw_prompt_var(std::format_string<targs> const fmt, targs&& ... vargs){
     throw_prompt(std::format(fmt,std::forward<targs>(vargs) ...);
};

而且:

throw_prompt_var("error {:d}:\t {:s}", 4000, "costume error");

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