【C++】编译阶段技巧:属性和静态断言

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

编译阶段作为C++程序生命周期的第三阶段(编码、预处理之后),其目标是生成计算机可识别的机器码(machine instruction
code)。

先来研究一下编译阶段究竟能做什么事情。

编译阶段编程

编译是预处理之后的阶段,它的输入是(经过预处理的)C++ 源码,输出是二进制可执行文件(也可能是汇编文件、动态库或者静态库)。这个处理动作就是由编译器来执行的。

和预处理阶段一样,在这里你也可以“面向编译器编程”,用一些指令或者关键字让编译器按照你的想法去做一些事情。只不过,这时你要面对的是庞大的 C++ 语法,而不是简单的文本替换,难度可以说是高了好几个数量级。

编译阶段的特殊性在于,它看到的都是 C++ 语法实体,比如 typedef、using、template、struct/class这些关键字定义的类型,而不是运行阶段的变量。所以,这时的编程思维方式与平常大不相同。我们熟悉的是CPU、内存、Socket,但要去理解编译器的运行机制、知道怎么把源码翻译成机器码,这可能就有点“强人所难”了。

所以这里我就只先学习一下比较容易理解的,属性和静态断言,非常使用!

属性

跟预处理阶段一样,编译阶段也有一些能够控制编译器的”编译指令“。不过在C++11的时候,才有官方版本(其他编译器自己实现的不做讨论),这个时候名字叫”属性“。(注意属性不同于宏,宏可以用户自定义,但属性只能使用内置的,自定义的属性标签上无法被编译器识别的)

简单理解的话,属性就是给变量、函数和类等贴上一个编译阶段的”标签“,以便编译器识别。

“属性”没有新增关键字,而是用两对方括号的形式“[[…]]”,方括号的中间就是属性标签(看着是不是很像一张方方正正的便签条),所以说用法很简单。

不过,在 C++11 里只定义了两个属性:“noreturn”和“carries_dependency”,它们基本上没什么大用处。

C++14的情况略微好了点,增加了一个比较实用的属性“deprecated”,用来标记不推荐使用的变量、函数或者类,也就是被“废弃”。我以前写Python的时候经常遇到,调用某个函数提示我说要废弃了,让我用另一个函数,原来C++这里可以用这样的方式实现。

比如下面这个,old_func()是我们要废弃的函数,就可以写成这样:

[[deprecated("deadline:xxxx-xx-xx")]]      // C++14 or later
int old_func();

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

warning: ‘int old_func()’ is deprecated: deadline:xxxx-xx-xx [-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属性,就是避免编译器放警告的。下面给出来constructer和destructer的测试代码(因为感觉自己用得上,最后一个暂时用不上):

g++ test.cpp -std=c++11 -o test
./test
#include <iostream>
using namespace std;

[[gnu::constructor]] void first_func()
{
    printf("before main()\n");
}

[[gnu::destructor]] void last_func()
{
    printf("after main()\n");
}

int main()
{
    cout << "i am in main funtion!" << endl;
    return 0;
}

程序运行结果:

before main()
i am in main funtion!
after main()

然后上面的printf不能换成cout,不然会出现”Segmentation fault (core dumped)“的错误。

静态断言

上面所说的属性,像是给编译器的一点”提示“,还不能说是编译阶段的编程,而静态断言就有点写程序的意思了。

相信大家读用过assert的用法,比如下面这个判断指针是否为空:

assert(p != nullptr);

当程序(也就是 CPU)运行到 assert 语句时,就会计算表达式的值,如果是 false,就会输出错误消息,然后调用 abort() 终止程序的执行。

注意,assert 虽然是一个宏,但在预处理阶段不生效,而是在运行阶段才起作用,所以又叫“动态断言”。

而这里要研究的是”静态断言“,名字很像,,叫“static_assert”,不过它是一个专门的关键字,而不是宏。因为它只在编译时生效,运行阶段看不见,所以是“静态”的。

“静态断言”有什么用呢?

类比一下 assert,你就可以理解了。它是编译阶段里检测各种条件的“断言”,编译器看到 static_assert 也会计算表达式的值,如果值是 false,就会报错,导致编译失败。

比如说,要想保证我们的程序只在 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.");  // 错误用法

不过静态断言最有用的地方,应该是在于检查模板的类型,比如说,断言是整数而不是浮点数、断言是指针而不是引用、断言类型可拷贝可移动……

这里需要配合标准库的type_traits,它提供了对应这些概念的各种编译器函数

#include <iostream>
#include <type_traits>
using namespace std;

#compile:g++ test.cpp -std=c++11 -o test

template <typename T>
void check_type(T v)
{
    using namespace std;

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

    cout << "static_assert : " << typeid(v).name() << endl;

    cout << is_void<void>::value << endl;
}

int main()
{
    cout << "in static_assert test!" << endl;
    check_type(10);
    return 0;
}

你可能看到了,“static_assert”里的表达式样子很奇怪,既有模板符号“<>”,又有作用域符号“::”,与运行阶段的普通表达式大相径庭,初次见到这样的代码一定会吓一跳。

这也是没有办法的事情。因为 C++本来不是为编译阶段编程所设计的。受语言的限制,编译阶段编程就只能“魔改”那些传统的语法要素了:把类当成函数,把模板参数当成函数参数,把“::”当成return 返回值。说起来,倒是和“函数式编程”很神似,只是它运行在编译阶段。

最后,静态断言可以作为编译期的一种约定,配合错误提示能够更快发现编译期的错误。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值