用可变参数模板替换可变参数

Replace varargs with variadic templates

我正在使用带有模板和递归的 C++17 来替换 C Va_Args。 目前仅支持浮点数,更多类型将在浮点数正常运行后支持 ;)

class CWrite
{
public:
    template<typename NextT, typename ...RestT>
    static std::string Format(NextT next, RestT ... rest);

private:

    template<typename T>
    static constexpr bool is_float = std::is_same_v<T, float>;

    template<typename T>
    static constexpr bool IsValidParam();

    template<typename LastT>
    static std::string Format(LastT last);

    ///Empty param case
    static std::string Format();

};

// +++++++++++++++++++  Implementation ++++++++++++++++++++++++++

template<typename T>
constexpr bool CWrite::IsValidParam()
{
    bool bRes = false;
    bRes |= is_float<T>;
    return bRes;
}

template<typename NextT, typename ...RestT>
std::string CWrite::Format(NextT next, RestT ... rest)
{
    std::string strRes = Format(next);
    strRes += Format(rest...);
    return strRes;
}

template<typename LastT>
std::string CWrite::Format(LastT last)
{

    std::string strRes;
    if (is_float<LastT>)
    {
        strRes = "float:";
        char buffer[10] = { };
        snprintf(buffer, 10, "%f", last);
        strRes += buffer;
    }

    return strRes;
}

///Empty param case
std::string CWrite::Format()
{
    return "";
}

调用它
std::string strRes = CWrite::Format(1.0f, 2.0f, 3.0f, 4.0f, 5);

导致 snprintf() 的警告 格式“%f”需要类型为“double”的参数,但参数 4 的类型为“int

我希望最后一个参数 IsValidParam returns false 应该是 int.

https://onlinegdb.com/B1A72GHgU

你能帮帮我吗? 我错过了什么吗?

如果你能使用C++17,你应该在下面的函数中使用if constexpr

template<typename LastT>
std::string CWrite::Format(LastT last)
{

    std::string strRes;

    // VVVVVVVVV  <-- add "constexpr" here
    if constexpr (is_float<LastT>)
    {
        strRes = "float:";
        char buffer[10] = { };
        snprintf(buffer, 10, "%f", last);
        strRes += buffer;
    }

    return strRes;
}

问题在于,使用简单的 if 而不是 if constexpr,编译器必须在 is_float<LastT> 时编译语句({ ... } 内的部分)是假的。

如果你不能使用C++17...我想你可以通过重载来区分函数

std::string CWrite::Format (float last)
 {    
   std::string strRes { "float:" };

   char buffer[10] = { };
   snprintf(buffer, 10, "%f", last);

   return strRes += buffer;
 }

std::string CWrite::Format (int last)
 {    
   std::string strRes { "int:" };

   char buffer[10] = { };
   snprintf(buffer, 10, "%i", last);

   return strRes += buffer;
 }

max66 的回答解决了您的方法格式字符串有问题的原因以及如何修复它。基本上,您只需要根据要格式化的值的类型选择不同的格式字符串。

但是,我想指出另一个缺陷:您假设任何给定值只需要 9 个字符即可转换为字符串。对于非常大的值(例如 1e22),这将失败。如果 GCC 可以在编译时确定这一点,它实际上会向您发出警告。

此外,您当前的实现分配了许多字符串并将它们递归地附加在一起。这当然是非常低效的,并且会降低 printf 系列函数的速度,以至于不值得使用它们。

此外,您的解决方案不检查格式错误(snprintf() returns 在这种情况下为负数)。在这种情况下,您可能会在字符串上附加未定义的内存,因为我不确定 C 标准是否保证在失败情况下终止缓冲区 (但它可能).

我的解决方案是使用一个函数将给定参数就地格式化到 std::string 的末尾。此外,它还处理格式错误和 9 个字节不足以保存格式化值的情况。

此外,我对参数类型施加了 SFINAE 限制,以确保只能使用我们支持的类型调用它。

这是我的解决方案,附有注释以解释作用和原因:

#include <string>
#include <type_traits>
#include <iostream>

// checks if T is a type we support
template<typename T>
inline constexpr bool allowed_type = std::is_floating_point_v<T> || std::is_integral_v<T>;

// the initial amount of space for stringifying each argument
constexpr std::size_t APPEND_PADDING = 20;

// returns the appropriate format string for type T (T assumed to be supported)
template<typename T>
const char *fmt_string()
{
    if constexpr (std::is_floating_point_v<T>) return "%f";
    else return "%d";
}

// stringifys val onto the end of str (T assumed to be supported)
template<typename T>
void append(std::string &str, T val)
{
    std::size_t prev_size = str.size();     // remember the previous size of str
    str.resize(prev_size + APPEND_PADDING); // allocate the space we need
    const char *fmt = fmt_string<T>();      // get the format string to use

    // format the value and check the save the return value
    int res = snprintf(&str[prev_size], APPEND_PADDING, fmt, val);

    // on format error, just skip it (or )
    if (res < 0) str.resize(prev_size);

    // if we didn't have enough room we need to try again with the correct size
    if ((std::size_t)res >= APPEND_PADDING)
    {
        str.resize(prev_size + res + 1); // make space for the characters we need and the null terminator
        snprintf(&str[prev_size], res + 1, fmt, val); // format the string again (this time will work)
        str.pop_back(); // remove the null terminator
    }
    // otherwise we had enough room, so just truncate to the written characters
    else str.resize(prev_size + res);
}

// formats all of args into a single string (only allows supported types)
template<typename ...Args, std::enable_if_t<(allowed_type<Args> && ...), int> = 0>
std::string format(Args ...args)
{
    std::string str;                               // create an empty buffer string to store the result
    str.reserve(sizeof...(args) * APPEND_PADDING); // predict how much space we'll need for everything
    int _[] = { (append(str, args), 0)... };       // append all the args to str one at a time
    (void)_; // suppress unused variable warnings (will just be optimized away)
    return str;
}

int main()
{
    std::cout << format(1, 2, 2.3, 3, 4.4, 5, 1e22) << '\n';
}

请注意,这会将所有格式化字符串一起运行,没有分隔。解决这个问题就像更改从 fmt_string().

返回的格式字符串一样简单

我使用的函数名称与您使用的不同,但您明白了。 format() 是您要使用的函数。