本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。
4.3 if constexpr
这是一个重点!
C++ 的编译时 if
!
此特性可以让你在编译时舍弃基于常量表达式条件的 if
语句的分支。
1 2 3 4
| if constexpr (cond) statement1; else statement2;
|
比如:
1 2 3 4 5 6 7 8
| template <typename T> auto get_value(T t) { if constexpr (std::is_pointer_v<T>) return *t; else return t; }
|
if constexpr
有能力简化许多模版代码——特别是用到标签调度(译注:tag dispatching)、SFINAE 或预处理器技巧时。
译注:
标签调度的示例。
SFINAE 的详细解释。
通俗地讲,都是实现模版特化/重载的技术手段。if constexpr
更轻量,允许在同一函数内使用 if 分支实现编译期条件选择。本文后边有一个这三种技术应用比较的例子。
为什么需要编译时 if ?
一开始你可能会问,为什么我们需要 if constexpr
,还有那些复杂的模版化的表达式?普通的 if
不行吗?
这里有一段示例代码:
1 2 3 4 5 6 7 8
| template <typename Concrete, typename... Ts> unique_ptr<Concrete> constructArgs(Ts&&... params) { if (is_constructible_v<Concrete, Ts...>) return make_unique<Concrete>(forward<Ts>(params)...); else return nullptr; }
|
上述例程是 make_unique
的“更新”版本:当参数允许它构造包装的对象时返回 unique_ptr
,否则返回 nullptr
。
下边是一份测试 constructArgs
的简单代码:
1 2 3 4 5 6 7 8 9 10
| class Test { public: Test(int, int) { } };
int main() { auto p = constructArgs<Test>(10, 10, 10); }
|
代码试图用 3 个参数构造 Test
,但是请注意 Test
只有一个接受两个 int
实参的构造函数。
编译时会得到类似如下的编译错误:
1 2 3 4 5 6
| In instantiation of 'typename std::_MakeUniq<_Tp>::__single_object std::make_unique(\ _Args&& ...) [with _Tp = Test; _Args = {int, int, int}; typename std::_MakeUniq<_Tp>\ ::__single_object = std::unique_ptr<Test, std::default_delete<Test> >]':
main.cpp:8:40: required from 'std::unique_ptr<_Tp> constructArgs(Ts&& ...) [with C\ oncrete = Test; Ts = {int, int, int}]'
|
让我们试着理解这条错误消息。在模板推断后,编译器编译出如下代码:
1 2 3 4
| if (std::is_constructible_v<Concrete, 10, 10, 10>) return std::make_unique<Concrete>(10, 10, 10); else return nullptr;
|
在运行时 if
分支永远不会被执行——因为 is_constructible_v
返回 false
,但是此分支内的代码必须能编译通过。
这就是为什么我们需要 if constexpr
,可以“舍弃”代码,只编译匹配的语句。
为了修复上边代码你必须添加 constexpr
:
1 2 3 4 5 6 7 8
| template <typename Concrete, typename... Ts> unique_ptr<Concrete> constructArgs(Ts&&... params) { if constexpr (is_constructible_v<Concrete, Ts...>) return make_unique<Concrete>(forward<Ts>(params)...); else return nullptr; }
|
现在,编译器在编译时计算出 if constexpr
的值,对 auto p = constructArgs<Test>(10, 10, 10);
这句来说,整个的 if 分支都在编译处理的第二个步骤里被“删除”了。
准确地说,被舍弃分支里的代码不是在编译阶段被完全删除了的。只有那些依赖于条件里用到的模板参数的表达式不会被实例化。但是要求语法上必须是有效的。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <typename T> void Calculate(T t) { if constexpr (is_integral_v<T>) { static_assert(sizeof(int) == 100); } else { execute(t); strange syntax } }
|
上边的伪代码里,如果 T 的类型是 int
,else 分支会被舍弃,意味着 execute(t)
不会被实例化。但是 strange syntax
这行仍然会被编译(因为它不依赖 T),这也是为什么你将会得到一个编译错误的原因。
然后,另一个编译错误从 static_assert
抛出,这个表达式也不依赖 T,所以它也会被(编译器)计算。
模版代码简化
C++17 之前,如果你有一个算法的多个版本——它们是依赖于类型需求的,你可能会通过 SFINAE 或标签调度生成候补重载决议集。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <typename T> std::enable_if_t<std::is_integral_v<T>, T> simpleTypeInfo(T t) { std::cout << "foo<integral T> " << t << '\n'; return t; }
template <typename T> std::enable_if_t<!std::is_integral_v<T>, T> simpleTypeInfo(T t) { std::cout << "not integral \n"; return t; }
|
上边的例子里有两个函数实现,但是只可能有一个会最终进入重载决议集。如果 std::is_integral_v<T>
为真就选择上边的函数,第二个因为 SFINAE 的缘故被拒绝。
同样的事发生在标签调度时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| template <typename T> T simpleTypeInfoTagImpl(T t, std::true_type) { std::cout << "foo<integral T> " << t << '\n'; return t; }
template <typename T> T simpleTypeInfoTagImpl(T t, std::false_type) { std::cout << "not integral \n"; return t; }
template <typename T> T simpleTypeInfoTag(T t) { return simpleTypeInfoTagImpl(t, std::is_integral<T>{}); }
|
不同于 SFINAE,我们为每一种条件生成一个唯一的标签类型:true_type
或 false_type
。基于结果,只有一个实现会被选择。
现在我们可以通过 if constexpr
简化这种模式:
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <typename T> T simpleTypeInfo(T t) { if constexpr (std::is_integral_v<T>) { std::cout << "foo<integral T> " << t << '\n'; } else { std::cout << "not integral \n"; } return t; }
|
模版代码的书写变得更“自然”了,而且也不需要那么多“伎俩”(译注:原文为 tricks)了。
示例
让我们看另外两个示例:
- 行打印
你可能已经在本书开头的快速开始小节看到过下边的示例了。让我们深入细节看看代码是怎么工作的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| template<typename T> void linePrinter(const T& x) { if constexpr (std::is_integral_v<T>) { std::cout << "num: " << x << '\n'; } else if constexpr (std::is_floating_point_v<T>) { const auto frac = x - static_cast<long>(x); std::cout << "flt: " << x << ", frac " << frac << '\n'; } else if constexpr(std::is_pointer_v<T>) { std::cout << "ptr, "; linePrinter(*x); } else { std::cout << x << '\n'; } }
|
linePrinter
利用 if constexpr
检查输入类型。基于此我们可以输出额外信息。有意思的是指针类型——当指针被检测到,代码对其解引用然后递归调用 linePrinter
。
- 声明自定义
get<N>
函数
结构化绑定对只有公有成员的简单结构体有效,比如:
1 2 3 4 5 6 7 8 9
| struct S { int n; std::string s; float d; };
S s; auto [a, b, c] = s;
|
但是,如果你有一个带私有成员的自定义类型,那你可能要重载 get<N>
函数以使结构化绑定能生效。看下边演示代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class MyClass { public: int GetA() const { return a; } float GetB() const { return b; } private: int a; float b; };
template <std::size_t I> auto get(MyClass& c) { if constexpr (I == 0) return c.GetA(); else if constexpr (I == 1) return c.GetB(); }
namespace std { template <> struct tuple_size<MyClass> : std::integral_constant<size_t, 2> { }; template <> struct tuple_element<0,MyClass> { using type = int; }; template <> struct tuple_element<1,MyClass> { using type = float; }; }
|
上边代码有一个优点:用一个函数完成所有工作。你也可以用模版特化实现:
1 2
| template <> auto& get<0>(MyClass &c) { return c.GetA(); } template <> auto& get<1>(MyClass &c) { return c.GetB(); }
|
在后边“用 if constexpr
替换 std::enable_if
” 一章中有更多相关示例,“结构化绑定”一章中也有,在关于自定义 get<N>
特化小节中。
你也可以参考这篇文章:C++17 中使用 if constexpr 简化代码。
扩展:本修改提案:P0292R2。