第18章 支持初始化语句的if和switch(C++17)
18.1 支持初始化语句的if
在C++17标准中,if控制结构可以在执行条件语句之前先执行一个初始化语句。语法如下:
if (init; condition) {}
允许初始化语句的if结构让以下代码成为可能:
#include <iostream>
bool foo()
{
return true;
}
int main()
{
if (bool b = foo(); b) {
std::cout << std::boolalpha << "good! foo()=" << b <<
std::endl;
}
}
在上面的代码中,bool b = foo()是一个初始化语句,在初始化语句中声明的变量b能够在if的作用域继续使用。事实上,该变量的生命周期会一直伴随整个if结构,包括else if和else部分。
if初始化语句中声明的变量拥有和整个if结构一样长的声明周期,所以前面的代码可以等价于:
#include <iostream>
bool foo()
{
return true;
}
int main()
{
{
bool b = foo();
if (b) {
std::cout << std::boolalpha << "good! foo()=" << b << std::endl;
}
}
}
还可以在if结构中添加else部分:
if (bool b = foo(); b) {
std::cout << std::boolalpha << "good! foo()=" << b <<
std::endl;
}
else {
std::cout << std::boolalpha << "bad! foo()=" << b <<
std::endl;
}
在else if条件语句之前也可以使用初始化语句:
#include <iostream>
bool foo()
{
return false;
}
bool bar()
{
return true;
}
int main()
{
if (bool b = foo(); b) {
std::cout << std::boolalpha << "foo()=" << b << std::endl;
}
else if (bool b1 = bar(); b1) {
std::cout << std::boolalpha
<< "foo()=" << b
<< ", bar()=" << b1 << std::endl;
}
}
在上面的代码中,if和else if都有初始化语句,它们分别初始化变量b和b1并且在各自条件成立的作用域内执行了日志输出。值得注意的是,b和b1的生命周期并不相同。其中变量b的生命周期会贯穿整个if结构(包括else if),可以看到在else if中也能引用变量b。但是b1则不同,它的生命周期只存在于else if以及后续存在的else if和else语句,而无法在之前的if中使用,等价于:
{
bool b = foo();
if (b) {
std::cout << std::boolalpha << "foo()=" << b << std::endl;
}
else {
bool b1 = bar();
if (b1) {
std::cout << std::boolalpha
<< "foo()=" << b
<< ", bar()=" << b1 << std::endl;
}
}
}
因为if初始化语句声明的变量会贯穿整个if结构,所以我们可以利用该特性对整个if结构加锁,例如:
#include <mutex>
std::mutex mx;
bool shared_flag = true;
int main()
{
if (std::lock_guard<std::mutex> lock(mx); shared_flag) {
shared_flag = false;
}
}
类似的例子还有:
#include <cstdio>
#include <string>
int main()
{
std::string str;
if (char buf[10]{0}; std::fgets(buf, 10, stdin)) {
str += buf;
}
}
18.2 支持初始化语句的switch
和if控制结构一样,switch在通过条件判断确定执行的代码分支之前也可以接受一个初始化语句。
#include <condition_variable>
#include <chrono>
using namespace std::chrono_literals;
std::condition_variable cv;
std::mutex cv_m;
int main()
{
switch (std::unique_lock<std::mutex> lk(cv_m); cv.wait_for(lk,100ms))
{
case std::cv_status::timeout:
break;
case std::cv_status::no_timeout:
break;
}
}
switch初始化语句声明的变量的生命周期会贯穿整个switch结构,这一点和if也相同,所以变量lk能够引用到任何一个case的分支中。
第19章 static_assert声明
19.1 运行时断言
运行时断言,只有程序运行起来之后才有可能触发它。通常情况下运行时断言只会在Debug模式下使用,因为断言的行为比较粗暴,它会直接显示错误信息并终止程序。
断言不能代替程序中的错误检查,它只应该出现在需要表达式返回true的位置
如果表达式中涉及外部输入,则不应该依赖断言,例如客户输入、服务端返回等:
void* resize_buffer(void* buffer, int new_size)
{
assert(buffer != nullptr); // OK,用assert检查函数参数
assert(new_size > 0);
assert(new_size <= MAX_BUFFER_SIZE);
…
}
bool get_user_input(char c)
{
assert(c == '\0x0d'); // 不合适,assert不应该用于
检查外部输入
…
}
19.2 静态断言的需求
运行时断言可以满足一部分需求,但是它有一个缺点就是必须让程序运行到断言代码的位置才会触发断言。如果想在模板实例化的时候对模板实参进行约束,这种断言是无法办到的。在C++11标准之前,没有一个标准方法来达到这个目的,我们需要利用其他特性来模拟。
#define STATIC_ASSERT_CONCAT_IMP(x, y) x ## y
#define STATIC_ASSERT_CONCAT(x, y) \
STATIC_ASSERT_CONCAT_IMP(x, y)
// 方案1
#define STATIC_ASSERT(expr) \
do { \
char STATIC_ASSERT_CONCAT( \
static_assert_var, __COUNTER__) \
[(expr) != 0 ? 1 : -1]; \
} while (0)
template<bool>
struct static_assert_st;
template<>
struct static_assert_st<true> {};
// 方案2
#define STATIC_ASSERT2(expr) \
static_assert_st<(expr) != 0>()
// 方案3
#define STATIC_ASSERT3(expr) \
static_assert_st<(expr) != 0> \
STATIC_ASSERT_CONCAT( \
static_assert_var, __COUNTER__)
以上代码的方案1,利用的技巧是数组的大小不能为负值,当expr表达式返回结果为false的时候,条件表达式求值为−1,这样就导致数组大小为−1,自然就会引发编译失败。方案2和方案3则是利用了C++模板特化的特性,当模板实参为true的时候,编译器能找到特化版本的定义。但当模板参数为false的时候,编译器无法找到相应的特化定义,从而编译失败。方案2和方案3的区别在于,方案2会构造临时对象,这让它无法出现在类和结构体的定义当中。而方案3则声明了一个变量,可以出现在结构体和类的定义中,但是它最大的问题是会改变结构体和类的内存布局。
19.3 静态断言
static_assert声明是C++11标准引入的特性,用于在程序编译阶段评估常量表达式并对返回false的表达式断言,我们称这种断言为静态断言。
1.所有处理必须在编译期间执行,不允许有空间或时间上的运行时成本。
2.它必须具有简单的语法。
3.断言失败可以显示丰富的错误诊断信息。
4.它可以在命名空间、类或代码块内使用。
5.失败的断言会在编译阶段报错。
C++11标准规定,使用static_assert需要传入两个实参:常量表达式和诊断消息字符串。
#include <type_traits>
class A {
};
class B : public A {
};
class C {
};
template<class T>
class E {
static_assert(std::is_base_of<A, T>::value, "T is not base of A");
};
int main(int argc, char *argv[])
{
static_assert(argc > 0, "argc > 0"); // 使用错误,argc>0不是常量表达式
E<C> x; // 使用正确,但由于A不是C的基类,所以触发断言
static_assert(sizeof(int) >= 4, // 使用正确,表达式返回真,不会触发失败断言
"sizeof(int) >= 4");
E<B> y; // 使用正确,A是B的基类,不会触发失败断言
}
19.4 单参数static_assert
在支持C++17标准的环境中,我们可以忽略第二个参数:
#include <type_traits>
class A {
};
class B : public A {
};
class C {
};
template<class T>
class E {
static_assert(std::is_base_of<A, T>::value);
};
int main(int argc, char *argv[])
{
E<C> x; // 使用正确,但由于A不是C的基类,会触发失败断言
static_assert(sizeof(int) < 4); // 使用正确,但表达式返回false,会触发失败断言
}
在GCC上,即使指定使用C++11标准,GCC依然支持单参数的static_assert。MSVC则不同,要使用单参数的static_assert需要指定C++17标准。