专业编程基础技术教程

网站首页 > 基础教程 正文

C++模板 - 9(变长模板参数)(c++可变长参数)

ccvgpt 2024-08-03 12:32:35 基础教程 21 ℃

前面介绍的类模板和函数模板的模板参数数量都是确定的,C++11之后我们迎来了一个重磅x级的特性,变长(variadic templates)模板参数。一个典型的例子就是std::tuple<...>,非常方便,通常用作多个数据成员的聚合体(这个让我想起了C#,它的Tuple<>最多支持17个类型参数,因为它只提供了最多17个类型参数的定义,比起C++的模板机制还是要逊色得多)。

下面看个例子,我们想把任意个参数的值累加起来(我可不想使用函数的重载,定义若干个参数长度的版本)

C++模板 - 9(变长模板参数)(c++可变长参数)

template<typename T>
T sum(T arg) {
    return arg;
}

template<typename T, typename... Types>
auto sum(T first, Types... args) {
    return first + sum(args...);
}

sum(3, 6.5, 35, 7); // 51.5

我们先定义了一个单参数的版本,返回值就是参数值本身。重点是第二个版本,这次typename...说明这里有若干个模板参数(包括0个)。Types称为模板参数包(template parameter pack)。在把这个模板参数包用于函数参数时,需要加上...,args称为函数参数包(function parameter pack)。

对于参数包,可用的操作并不多,...是一个,称为包展开,编译器把里面的个体一个一个列出来,用逗号分隔(在任何支持逗号操作符的地方都可以使用它)。还有一个操作符是sizeof...,也是同时引入的,可以得到包里面的元素数量。

下面简单说一下sum(3, 6.5, 35, 7)会发生什么。编译器会生成一个sum(int, double, int, int)版本,然后再生成一个sum(double, int, int),如此递归至生成一个sum(int, int)。最后一个sum(int)由第一个版本的模板函数生成,然后反向使用+操作符计算得到最后结果。

下面我们使用sizeof...来简化一下sum的定义,看能不能把两个版本整合成一个?

template<typename T, typename... Types>
auto sum(T first, Types... args) {
    if (sizeof...(args) == 0) {
        return first;
    } else {
        return first + sum(args...);
    }
}

上面能通过编译吗?请思考一分钟!

时间到,不能!为什么?逻辑上看起来没有任何问题呀。if-else是运行时的路径选择,编译器编译时会生成两条路径中的所有代码。当运行到sizeof...(args) == 0时,虽然else的路径不会被执行,但是里面的代码编译器同样要生成。而这个时候编译器需要生成sum的0参数版本,但是它无法生成,因为按照sum的定义,sum至少有一个first参数。所以在args...为空的时候,虽然并不会执行到else,但是编译器还是兢兢业业的给我们生成else里的代码,sum()让它难办了。

看来我们真正需要的是编译期的路径选择,也就是当args的数量为0时,根本不生成else中的代码。C++17带来了好消息!

template<typename T, typename... Types>
auto sum(T first, Types... args) {
    if constexpr (sizeof...(args) == 0) {
        return first;
    } else {
        return first + sum(args...);
    }
}

这回一切都OK了。if constexpr实现编译期的路径选择,不过里面的条件表达式必须是编译期可计算的。

Tags:

最近发表
标签列表