Simplify Code with “if constexpr” in C++17
Introduction
程序设计需要不断地做抉择,抉择便需用到逻辑分派。
Modern C++ 中,有多种方式完成这个任务,例如 Run-time if,Tag dispatching,SFINAE,Partial Specialization 等等。这些方式分为运行期分派和编译期分派,分派的条件称为约束。设计是为了厉行约束,理想上,能在编译期强制表现的约束就应提升到编译期完成。
C++17 又增加了一种编译期方式 Compile-time if;C++20 中,又增加了 Concepts。
本篇就来总结一下各种方式的用法异同(Concepts见下篇)。
Run-time if
运行期 if
就是运行时期才会执行的分支语句,例如:
template <typename T>
void foo(const T& val)
{
if(std::is_integral<T>::value) {
std::cout << "integral\n";
} else {
std::cout << "non-integral\n";
}
}
if-else
和 switch
都可以在运行期进行逻辑分派,大部分时候的执行期成本亦微不足道。但是 if-else
要求每一个分支都得编译成功,即使不会执行到的分支,比如返回两个不同类型值的时候,便会出现编译错误。
下面,来看一个贯穿本文的例子。
C++ 有一个 stringstream
字符串流,可以用来读取和写出字符流,我们可以利用它来写一个类型转换工具(C++11 就有一个 std::is_convertible
)。
首先,需要判断类型是否可以直接相互转换(包含隐式转换),可以直接转换的则直接转换,不可直接转换的则利用 stringstream
转换。我们使用 convertible
来完成该工作:
template <class T, class U>
class convertible
{
using small_type = char;
class big_type { char dummy[2]; };
static small_type test(T);
static big_type test(...);
static U makeU();
public:
enum { value = sizeof(test(makeU())) == sizeof(small_type) };
};
这里使用了两个稻草人函数 test()
来判断类型是否可以进行转换,两个稻草人函数返回的类型大小不同,如果 U
可隐式转换为 T
,则会返回 small_type
;若不可转换,则会调用任意参版本,返回 big_type
。通过对比两者大小,便能判断两个类型是否能够进行转换。
现在,通过 Run-time if 来完成实际转换:
// Run-time if
template <class T, class U>
void convert(T& to, const U& from)
{
if(convertible<T, U>::value) {
to = from; // error if false!
} else {
std::stringstream ss;
ss << from;
ss >> to;
}
}
int main()
{
int val;
std::string str{"0706"};
convert(val, str);
// output:
std::cout << val << std::endl;
}
这里,如果 convertible
的结果为 false
,则会编译失败,因为 if-else
的每个分支都要编译通过,即使不会被执行到。
Tag Dispatching
可以使用 tag 来进行编译期流程分派,从而解决 Run-time if 所存在的问题。实现如下:
// Tag dispatching
template <bool v>
struct type_tag { enum { value = v}; };
template <class T, class F>
void convert_impl(T& to, const F& from, type_tag<false>)
{
std::stringstream ss;
ss << from;
ss >> to;
}
template <class T, class F>
void convert_impl(T& to, const F& from, type_tag<true>)
{
to = from;
}
template <class T, class F>
void convert(T& to, const F& from)
{
convert_impl(to, from, type_tag<convertible<T, F>::value>());
}
通过 type_tag
将数值转换为型别,由此可以将型别的一个暂时对象传递给重载函数,从而实现编译期分派。测试如下:
int main()
{
int val;
std::string str{"0706"};
double pi = 3.1415;
convert(val, str);
std::cout << val << std::endl; // output: 706
convert(val, pi);
std::cout << val << std::endl; // output: 3
}
SFINAE
与 Tag-dispatching 密切相关的是 SFINAE,借其实现上述需求,代码如下:
// SFINAE
template <class T, class F,
typename std::enable_if_t<!convertible<T, F>::value, bool> = true>
void convert(T& to, const F& from)
{
std::stringstream ss;
ss << from;
ss >> to;
}
template <class T, class F,
typename std::enable_if_t<convertible<T, F>::value, bool> = true>
void convert(T& to, const F& from)
{
to = from;
}
使用 SFINAE 的效果和 Tag-dispatching 相同,但工作方式不同,Tag-dispatching 使用参数推导来选择合适的 helper 重载,而 SFINAE 直接对主函数操纵重载集。SFINAE 使用起来并不方便,且不易读,有时还需要伴随着一些技巧,不过到了 C++17 以后,我们有更好的武器。
Partial Specialization
另一种方法是直接使用偏特化,通过仿函数来实现逻辑分派。
// Partial specialazation
template <class T, class F, bool> struct convert_functor {};
template <class T, class F>
struct convert_functor<T, F, true>
{
void operator()(T& to, const F& from) const
{
to = from;
}
};
template <class T, class F>
struct convert_functor<T, F, false>
{
void operator()(T& to, const F& from) const
{
std::stringstream ss;
ss << from;
ss >> to;
}
void operator()(std::string& to, const F& from) const
{
std::stringstream ss;
ss << from;
to = ss.str();
}
};
template <class T, class F>
void convert(T& to, const F& from)
{
convert_functor<T, F, convertible<T, F>::value>()(to, from);
}
这种方式最为灵活,但即使只需一个简单二元分派,所需代码相较亦多。
Compile-time if
Compile-Time if 是 C++17 的特性,可以使用它来简单地实现上述需求:
// Compile-time if
template <class T, class F>
void convert(T& to, const F& from)
{
if constexpr (convertible<T, F>::value) {
to = from;
} else {
std::stringstream ss;
ss << from;
ss >> to;
}
}
与 Run-time if 一样,所有代码都可写于一处,无需像 Tag-dispatching,Partial specialization 那样添加额外的 helper 辅助函数。可以使用 Compile-Time if 的情境下,应该优先考虑使用,以简化代码。但是依旧有一些需要注意的地方。举个例子:
template <typename T>
constexpr auto foo(const T& val)
{
if constexpr (std::is_integral<T>::value)
{
if constexpr (T{} < 10) {
return val * 2;
}
}
return val;
}
这样使用起来完全没问题:
constexpr auto a = foo(10); // 20
constexpr auto b = foo("hi"); // hi
而如果合并两个if,结果便不如所愿:
template <typename T>
constexpr auto foo(const T& val)
{
if constexpr (std::is_integral<T>::value && T{} < 10)
{
return val * 2;
}
return val;
}
相同的调用,constexpr auto b = foo("hi");
会出现编译错误。这是由于 Compile-time if 在条件实例化时需要整个条件都有效,而 T{} < 10
不满足,是以当有多个编译期条件时,应该采用嵌入多个 if
的方式编写代码。
除了 Compile-Time if,C++20 的 Concepts 允许使用简单的语法对模板添加任意 requirements/conditions,下篇来看。
1 thought on “Simplify Code with “if constexpr” in C++17”