加载中...

编译阶段能做什么:属性和静态断言

属性(attribute)

属性“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-decl

“属性”也支持非标准扩展,允许以类似名字空间的方式使用编译器自己的一些“非官方”属性,比如,GCC 的属性都在“gnu::”里。下面我就列出几个比较有用的.

deprecated:与 C++14 相同,但可以用在 C++11 里。
unused:用于变量、类型、函数等,表示虽然暂时不用,但最好保留着,因为将来可能
会用。
constructor:函数会在 main() 函数之前执行,效果有点像是全局对象的构造函数。
destructor:函数会在 main() 函数结束之后执行,有点像是全局对象的析构函数。
always_inline:要求编译器强制内联函数,作用比 inline 关键字更强。
hot:标记“热点”函数,要求编译器更积极地优化。

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

静态断言(static_assert)

assert属于动态断言,用来断言一个表达式必定为真。比如说,数字必须是正数,指针必须非空、函数必须返回 true,当程序(也就是 CPU)运行到 assert 语句时,就会计算表达式的值,如果是 false,就会输出错误消息,然后调用 abort() 终止程序的执行。

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

有了“动态断言”,那么相应的也就有“静态断言”,名字也很像,叫“static_assert”,不过它是一个专门的关键字,而不是宏。因为它只在编译时生效,运行阶段看不见,所以是“静态”的。如下斐波拉契数列计算函数,可以用静态断言来保证模板参数必须大于等于零:

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."); // 错误用法

说到这儿,你大概对 static_assert 的“编译计算”有点感性认识了吧。在用“静态断言”的时候,你就要在脑子里时刻“绷紧一根弦”,把自己代入编译器的角色,像编译器那样去思考,看看断言的表达式是不是能够在编译阶段算出结果。不过这句话说起来容易做起来难,计算数字还好说,在泛型编程的时候,怎么检查模板类型呢?比如说,断言是整数而不是浮点数、断言是指针而不是引用、断言类型可拷贝可移动……
这些检查条件表面上看好像是“不言自明”的,但要把它们用 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 返回值。说起来,倒是和“函数式编程”很神似,只是它运行在编译阶段。

小结

1.“属性”相当于编译阶段的“标签”,用来标记变量、函数或者类,让编译器发出或者
不发出警告,还能够手工指定代码的优化方式。
2.官方属性很少,常用的只有“deprecated”。我们也可以使用非官方的属性,需要加上
名字空间限定。
3.static_assert 是“静态断言”,在编译阶段计算常数和类型,如果断言失败就会导致编
译错误。它也是迈向模板元编程的第一步。
4.和运行阶段的“动态断言”一样,static_assert 可以在编译阶段定义各种前置条件,充
分利用 C++ 静态类型语言的优势,让编译器执行各种检查,避免把隐患带到运行阶段。
5.不要太过于依赖assert来检测或者预防错误,而是要把它作为一种“文档形式的代码”,显式地表
明前提条件或后续结果。

posted @ 2022-04-15 13:01  江上莲花香  阅读(52)  评论(0)    收藏  举报