C++ 中的可变参数模板和逗号分隔的字符串

Variadic templates in C++ and a comma separated string

我正在努力研究可变参数模板,并认为一个应该采用任意参数(不同类型)的简单函数将是一个很好的练习。

第一次尝试

template<typename T>
typename std::enable_if<std::is_integral<T>::value, std::string>::type
concater ( T x ) { return std::to_string(x); }

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, std::string>::type
concater ( T x ) { return std::to_string(x); }

template<typename T>
typename std::enable_if<std::is_convertible<T,std::string>::value, std::string>::type
concater ( T x ) { return std::string(x); }

template<typename T, typename... Rest>
typename std::enable_if<std::is_integral<T>::value, std::string>::type
concater ( T x, Rest... xs ) { return std::to_string(x) + "; " + concater( xs... ); }

template<typename T, typename... Rest>
typename std::enable_if<std::is_floating_point<T>::value, std::string>::type
concater ( T x, Rest... xs ) { return std::to_string(x) + "; " + concater( xs... ); }

template<typename T, typename... Rest>
typename std::enable_if<std::is_convertible<T,std::string>::value, std::string>::type
concater ( T x, Rest... xs ) { return std::string(x) + "; " + concater( xs... ); }

template<typename... Values>
std::string to_csv ( Values... vs )
{
    return concater( vs... );
}

很好用,但不是很好。它也可能不会推广到其他类型。所以我进行了第二次尝试,并认为使用 << 运算符会使函数更通用。

第二次尝试

template<typename Stream, typename T>
void concat2 ( Stream & s, T x ) { s << x; }

template<typename Stream, typename T, typename... Rest>
void concat2 ( Stream & s, T x, Rest... xs )
{
    s << x << ";";
    concat2( s, xs... );
}

template<typename... Values>
std::string to_csv2 ( Values... vs )
{
    std::ostringstream oss;
    concat2( oss, vs... );
    return oss.str();
}

在我看来,这个更短,看起来更漂亮。但似乎还是差了一点点。您将如何完成这项任务,或者有什么方法可以让它变得更好?

我也想知道是不是这样:

std::cout << to_csv(1,"Hello",3) << std::endl;

会被编译成类似这样的东西:

std::cout << 1 << "Hello" << 3 << std::endl;

有办法实现吗?

第二段代码几乎是我在处理递归可变参数模板时使用的模式。您绝对 必须 有一个剥离参数的可变参数模板,以及另一个采用静态数量参数的模板,一旦可变参数模板用完额外参数,就会调用该模板。拥有一个额外的功能来像您一样将功能整齐地包装在蝴蝶结中通常也很不错,尤其是在编译器存在自动参数类型推导问题的情况下。

关于你的第二个问题:

std::cout << to_csv(1,"Hello",3) << std::endl;

相当于

void concat ( Stream & s, int x ) { s << x; }

void concat ( Stream & s, std::string x, int y )
{
    s << x << ";";
    concat( s, y );
}

void concat ( Stream & s, int x, std::string y, int z )
{
    s << x << ";";
    concat( s, y, z );
}

std::string to_csv ( int x, std::string y, int z )
{
    std::ostringstream oss;
    concat( oss, x, y, z );
    return oss.str();
}
//...
std::cout << to_csv(1,"Hello",3) << std::endl;

重要的是要注意您的可变参数模板使用正在生成所有这些函数。虽然当您使用积极优化的编译器编译代码以发布时,这通常对运行时影响很小,但它可能会导致调试构建的速度严重减慢。

再一次,braced-init-list 中的包扩展来拯救。

template<typename Value, typename... Values>
std::string to_csv2 ( Value v, Values... vs )
{
    std::ostringstream oss;
    using expander = int[];
    oss << v; // first
    (void) expander{ 0, (oss << ";" << vs, void(), 0)... };
    return oss.str();
}

Demo.

思路是先打印第一个元素;然后在单个包扩展中打印剩余的元素,每个元素前面都有分隔符。我们在临时数组的初始值设定项列表中扩展模式 (oss << ";" << vs, void(), 0),因此给定 vs 作为包含 v1, v2, v3 的包,它扩展为

(void) expander{ 0, (oss << ";" << v1, void(), 0),
                    (oss << ";" << v2, void(), 0),
                    (oss << ";" << v3, void(), 0) };

括号内的逗号是逗号运算符,对左操作数求值,舍弃结果,再对右操作数求值。 void() 防止可能存在的任何逗号运算符重载(因为我们不知道值的类型以及 oss << vs 可能 return 的内容)。

这些表达式中的每一个都计算为 0,即逗号运算符最右边的操作数,用于初始化临时数组中我们实际上并不关心的元素。如果 vs 是空包,则需要第一个 0 以确保我们不会创建非法的零大小数组。

最终结果是评估每个初始化子句会将前面带有分隔符的相应变量输出到流中,并且标准中严格保证这些初始化子句是从左到右评估的,所以我们将以正确的顺序将值发送到流。