第15.8章 if constexpr 与条件错误契约

English

1. 不被编译的那个分支

第7.3章把 if constexpr 介绍成编译期 if。在模板里它多做一件事:没被选中的 分支会被丢弃,而不是编译,所以它对当前模板实参来说可以是非法代码。

这就是它与普通 if 的全部差别。运行期 if 的两个分支都必须对每个 类型都编译通过;if constexpr 只要被选中的那个能编译。泛型函数就是靠它说 “如果类型是整型就做这个,否则做那个”,而不用给每个类别写一个函数:


template <typename T>
[[nodiscard]] inline constexpr ::std::string_view describe() noexcept
{
	if constexpr (::std::integral<T>)
	{
		return "integral";
	}
	else if constexpr (::std::floating_point<T>)
	{
		return "floating point";
	}
	else
	{
		return "something else";
	}
}

describe<no_size>() 返回 "something else", 前两个分支在那个实例化里根本不存在 —— 不编译、不生成、也到不了。

2. if constexpr 里的 requires

条件可以是临时 requires 表达式 (第15.3章), 它问的是“这个表达式对这个类型合法吗”。泛型代码就是这样发现一个可选成员函数的, 而不必给调用者加约束:


template <typename T>
[[nodiscard]] inline constexpr ::std::size_t element_count() noexcept
{
	if constexpr (requires(T const &t) { t.size(); })
	{
		return 8u;
	}
	else
	{
		return 1u;
	}
}

::std::string_view 有 size(),走第一个分支; no_size 没有,走第二个。没有一处写了两次,而且某个类型明天多了 size(),这个函数不用改就会开始走第一个分支。

3. 取决于实例化的契约

第6.16章说每个函数要么 noexcept 要么 throws。对模板来说这是麻烦事:对 int 不会失败的操作,对用户类型可能失败。答案是条件异常说明:


template <typename T>
inline void check_nonzero(T value) throws(::std::is_integral_v<T>)
{
	if constexpr (::std::is_integral_v<T>)
	{
		if (value == 0) [[unlikely]]
		{
			throw throws ::std::errc::invalid_argument;
		}
	}
	else
	{
		static_cast<void>(value);
	}
}

throws(condition) 的含义是:条件为 true 时函数可以抛赫布异常; 为 false 时函数是 noexcept,并由编译器强制执行。 两个实例化是真正不同类型的函数:


noexcept(check_nonzero<::std::int_least32_t>(7))   // false
noexcept(check_nonzero<::std::float64_t>(1.0))     // true

为什么值得?因为 noexcept 才让一个操作能用在另一个 noexcept 操作里,也因为 ::std::is_nothrow_move_constructible_v 之类 (第15.1章)正是容器在移动与 拷贝之间做选择的依据。一个无条件 throws 的模板,会强迫装它的容器在扩容时拷贝。

4. 继承被包装调用的契约

最常见的条件异常说明与类型无关,与被转发的调用有关。noexcept(表达式) 是一个运算符:该表达式不会抛时得到 true。于是完美转发的包装函数可以承诺 与它调用的那次调用完全一样的强度:


template <typename output, typename T>
inline void print_once(output &&out, T const &value) throws(!noexcept(::fast_io::io::print(out, value)))
{
	::fast_io::io::print(out, value);
}

throws(!noexcept(...)) 读起来别扭,但意思就是它说的: “除非被包装的调用不会抛,否则本函数可以抛”。如果你更喜欢,也可以写成 noexcept(noexcept(...)) —— 在赫布异常工具链上两种写法一致。

fast_io 自己的头文件里到处是这个模式,只是藏在宏后面,以便库能同时面向两种异常模型构建:


// fast_io_dsal/impl/misc/push_macros.h, herbceptions branch
#define FAST_IO_HERBCEPTIONS_THROWS_IF(...) throws(__VA_ARGS__)
#define FAST_IO_HERBCEPTIONS_THROWS_IF_NOT_NOEXCEPT(...) throws(!noexcept(__VA_ARGS__))

// legacy branch
#define FAST_IO_HERBCEPTIONS_THROWS_IF(...) noexcept(!(__VA_ARGS__))
#define FAST_IO_HERBCEPTIONS_THROWS_IF_NOT_NOEXCEPT(...) noexcept(noexcept(__VA_ARGS__))

::fast_io::native_global_allocator 给每个分配函数都加上 FAST_IO_HERBCEPTIONS_THROWS_IF(allocator::throws_on_allocation_failure), 这就是为什么 ::fast_io::vector<T> 是 throws, 而建立在“永不失败”分配器上的 vector 是 noexcept (第15.9章给出那段代码)。

5. 分支里放什么

条件 用来做什么
::std::integral<T>、::std::floating_point<T> 按类型类别分开算法
::std::is_pointer_v<T>、sizeof(T) == N 布局决策、挑选内建指令
requires { t.size(); } 可选成员、能力探测
::std::is_constant_evaluated() 编译期用另一套算法 (第7.1章)
!noexcept(expr) 镜像某次调用的错误契约

两条规矩能让 if constexpr 不至于变成新的 #ifdef 面条。 第一,走不到的分支是死代码:它不编译,所以里面的笔误要等到有人用匹配的类型实例化时才会被发现。 第二,超过三个分支的链是设计味道 —— 要么这些类别该变成概念加重载(重载会按包含关系自动排序), 要么这个类型本该自己提供那个操作。

6. 完整程序


#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <stdfloat>
#include <type_traits>

struct no_size
{
	int value{};
};

template <typename T>
[[nodiscard]] inline constexpr ::std::string_view describe() noexcept
{
	if constexpr (::std::integral<T>)
	{
		return "integral";
	}
	else if constexpr (::std::floating_point<T>)
	{
		return "floating point";
	}
	else
	{
		return "something else";
	}
}

template <typename T>
[[nodiscard]] inline constexpr ::std::size_t element_count() noexcept
{
	if constexpr (requires(T const &t) { t.size(); })
	{
		return 8u;
	}
	else
	{
		return 1u;
	}
}

// the error contract depends on the instantiation
template <typename T>
inline void check_nonzero(T value) throws(::std::is_integral_v<T>)
{
	if constexpr (::std::is_integral_v<T>)
	{
		if (value == 0) [[unlikely]]
		{
			throw throws ::std::errc::invalid_argument;
		}
	}
	else
	{
		static_cast<void>(value);
	}
}

int main()
{
	try
	{
		::fast_io::io::println(describe<int>());
		::fast_io::io::println(describe<::std::float64_t>());
		::fast_io::io::println(describe<no_size>());
		::fast_io::io::println("element_count<no_size>=", element_count<no_size>());
		::fast_io::io::println("element_count<string_view>=", element_count<::std::string_view>());
		::fast_io::io::println("noexcept(check_nonzero<int>)=", noexcept(check_nonzero<::std::int_least32_t>(1)));
		::fast_io::io::println("noexcept(check_nonzero<float64_t>)=", noexcept(check_nonzero<::std::float64_t>(1.0)));

		check_nonzero<::std::int_least32_t>(7);
		::fast_io::io::println("check_nonzero<int>(7) returned");
		try
		{
			check_nonzero<::std::int_least32_t>(0);
		}
		catch throws (::std::error)
		{
			::fast_io::io::println("zero rejected");
		}
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln(e);
	}
}

输出:


integral
floating point
something else
element_count<no_size>=1
element_count<string_view>=8
noexcept(check_nonzero<int>)=0
noexcept(check_nonzero<float64_t>)=1
check_nonzero<int>(7) returned
zero rejected

处理器收到的是 [posix](22)Invalid argument,即标准为 ::std::errc::invalid_argument 给出的文本;程序只打印自己的消息, 好让上面的运行记录只有一条流。

7. 经验法则