编译阶段:属性和静态断言

编译阶段的目标是生成计算机可识别的机器码(machine instruction code)。而编码阶段和预处理阶段的主要工作还是“文本编辑”,生成的是人类可识别的源码(source code)。编译阶段的特殊性在于,它看到的都是 C++ 语法实体,比如 typedef、using、template、struct/class 这些关键字定义的类型,而不是运行阶段的变量。所以,这时的编程思维方式与平常大不相同。

比如说,让编译器递归计算斐波那契数列,这已经算是一个比较容易理解的编译阶段数值计算用法了:

template<int N>
struct fib                   // 递归计算斐波那契数列
{
    static const int value =
        fib<N - 1>::value + fib<N - 2>::value;
};

template<>
struct fib<0>                // 模板特化计算fib<0>
{
    static const int value = 1;
};

template<>
struct fib<1>               // 模板特化计算fib<1>
{
    static const int value = 1;
};

// 调用后输出2,3,5,8
cout << fib<2>::value << endl;
cout << fib<3>::value << endl;
cout << fib<4>::value << endl;
cout << fib<5>::value << endl;

对于编译器来说,可以在一瞬间得到结果,但你要搞清楚它的执行过程,就得在大脑里把 C++ 模板特化的过程走一遍。整个过程无法调试,完全要靠自己去推导,特别“累人”。(你也可以把编译器想象成是一种特殊的“虚拟机”,在上面跑的是只有编译器才能识别、处理的代码。)

属性(attribute)

在 C++11 之前,标准里没有规定这样的东西,但 GCC、VC 等编译器发现这样做确实很有用,于是就实现出了自己“编译指令”,在 GCC 里是“__ attribute __”,在 VC 里是“__declspec”。不过因为它们不是标准,所以名字显得有点“怪异”。到了 C++11,标准委员会终于认识到了“编译指令”的好处,于是就把“民间”用法升级为“官方版本”,起了个正式的名字叫“属性”。你可以把它理解为给变量、函数、类等“贴”上一个编译阶段的“标签”,方便编译器识别处理。“属性”没有新增关键字,而是用两对方括号的形式“[[…]]”,方括号的中间就是属性标签(看着是不是很像一张方方正正的便签条)。所以,它的用法很简单,比 GCC、VC 的都要简洁很多。

[[noreturn]]              // 属性标签
int func(bool flag)       // 函数绝不会返回任何值
{
    throw std::runtime_error("XXX");
}

在 C++11 里只定义了两个属性:“noreturn”和“carries_dependency”,它们基本上没什么大用处。C++14 的情况略微好了点,增加了一个比较实用的属性“deprecated”,用来标记不推荐使用的变量、函数或者类,也就是被“废弃”。比如说,你原来写了一个函数 old_func(),后来觉得不够好,就另外重写了一个完全不同的新函数。但是,那个老函数已经发布出去被不少人用了,立即删除不太可能,该怎么办呢?这个时候,你就可以让“属性”发挥威力了。你可以给函数加上一个“deprecated”的编译期标签,再加上一些说明文字:

[[deprecated("deadline:2020-12-31")]]      // C++14 or later
int old_func();

于是,任何用到这个函数的程序都会在编译时看到这个标签,报出一条警告:

warning:int old_func()’ is deprecated: deadline:2020-12-31 [-Wdeprecated-declarations]

当然,程序还是能够正常编译的,但这种强制的警告形式会“提醒”用户旧接口已经被废弃了,应该尽快迁移到新接口。显然,这种形式要比毫无约束力的文档或者注释要好得多。目前的 C++17 和 C++20 又增加了五六个新属性,比如 fallthrough、likely,但我觉得,标准委员会的态度还是太“保守”了,在实际的开发中,这些真的是不够用。好在“属性”也支持非标准扩展,允许以类似名字空间的方式使用编译器自己的一些“非官方”属性,比如,GCC 的属性都在“gnu::”里。下面我就列出几个比较有用的(全部属性可参考GCC 文档)

deprecated:与 C++14 相同,但可以用在 C++11 里。

unused:用于变量、类型、函数等,表示虽然暂时不用,但最好保留着,因为将来可能会用。

constructor:函数会在 main() 函数之前执行,效果有点像是全局对象的构造函数。

destructor:函数会在 main() 函数结束之后执行,有点像是全局对象的析构函数。

always_inline:要求编译器强制内联函数,作用比 inline 关键字更强。

hot:标记“热点”函数,要求编译器更积极地优化。

以上用法说明

以unused为例,在没有这个属性的时候,如果有暂时用不到的变量,我们只能用“(void) var;”的方式假装用一下,来“骗”过编译器,属于“不得已而为之”的做法。那么现在,我们就可以用“unused”属性来清楚地告诉编译器:这个变量我暂时不用,请不要过度紧张,不要发出警告来烦我:

[[gnu::unused]]      // 声明下面的变量暂不使用,不是错误
int nouse;  

动态断言(dynamic_assert)

“属性”像是给编译器的一个“提示”“告知”,无法进行计算,还算不上是编程,而接下来要讲的“静态断言”,就有点编译阶段写程序的味道了。

assert()用来断言一个表达式必定为真。比如说,数字必须是正数,指针必须非空、函数必须返回 true:

assert(i > 0 && "i must be greater than zero");
assert(p != nullptr);
assert(!str.empty());

当程序(也就是 CPU)运行到 assert 语句时,就会计算表达式的值,如果是 false,就会输出错误消息,然后调用 abort() 终止程序的执行。注意,assert 虽然是一个宏,但在预处理阶段不生效,而是在运行阶段才起作用,所以又叫“动态断言”。

静态断言(static_assert)

类比一下 assert,它是编译阶段里检测各种条件的“断言”,编译器看到 static_assert 也会计算表达式的值,如果值是 false,就会报错,导致编译失败。不过它是一个专门的关键字,而不是宏。因为它只在编译时生效,运行阶段看不见,所以是“静态”的。

比如说,这节课刚开始时的斐波拉契数列计算函数,可以用静态断言来保证模板参数必须大于等于零:

template<int N>
struct fib
{
    static_assert(N >= 0, "N >= 0");

    static const int value =
        fib<N - 1>::value + fib<N - 2>::value;
};

再比如说,要想保证我们的程序只在 64 位系统上运行,可以用静态断言在编译阶段检查 long 的大小,必须是 8 个字节(当然,你也可以换个思路用预处理编程来实现)。

static_assert(
  sizeof(long) >= 8, "must run on x64");
  
static_assert(
  sizeof(int)  == 4, "int must be 32bit");

注意,static_assert 运行在编译阶段,只能看到编译时的常数和类型,看不到运行时的变量、指针、内存数据等,是“静态”的,所以不要简单地把 assert 的习惯搬过来用。比如,下面的代码想检查空指针,由于变量只能在运行阶段出现,而在编译阶段不存在,所以静态断言无法处理。就像编译器那样去运行,看看断言的表达式是不是能够在编译阶段算出结果。

char* p = nullptr;
static_assert(p == nullptr, "some error.");  // 错误用法

不过这句话说起来容易做起来难,计算数字还好说,在泛型编程的时候,怎么检查模板类型呢?比如说,断言是整数而不是浮点数、断言是指针而不是引用、断言类型可拷贝可移动……这些检查条件表面上看好像是“不言自明”的,但要把它们用 C++ 语言给精确地表述出来,可就没那么简单了。所以,想要更好地发挥静态断言的威力,还要配合标准库里的“type_traits”,它提供了对应这些概念的各种编译期“函数”。

// 假设T是一个模板参数,即template<typename T>

static_assert(
  is_integral<T>::value, "int");

static_assert(
  is_pointer<T>::value, "ptr");

static_assert(
  is_default_constructible<T>::value, "constructible");

“static_assert”里的表达式样子很奇怪,既有模板符号“<>”,又有作用域符号“::”,与运行阶段的普通表达式大相径庭,初次见到这样的代码一定会吓一跳。这也是没有办法的事情。因为 C++ 本来不是为编译阶段编程所设计的。受语言的限制,编译阶段编程就只能“魔改”那些传统的语法要素了:把类当成函数,把模板参数当成函数参数,把“::”当成 return 返回值。说起来,倒是和“函数式编程”很神似,只是它运行在编译阶段。由于“type_traits”已经初步涉及模板元编程的领域,不太好一下子解释清楚,所以,在这里就不再深入介绍了。

总结

“属性”相当于编译阶段的“标签”,用来标记变量、函数或者类,让编译器发出或者不发出警告,还能够手工指定代码的优化方式。

官方属性很少,常用的只有“deprecated”。我们也可以使用非官方的属性,需要加上名字空间限定。

static_assert 是“静态断言”,在编译阶段计算常数和类型,如果断言失败就会导致编译错误。它也是迈向模板元编程的第一步。

和运行阶段的“动态断言”一样,static_assert 可以在编译阶段定义各种前置条件,充分利用 C++ 静态类型语言的优势,让编译器执行各种检查,避免把隐患带到运行阶段。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值