9 装饰器
约 4255 字大约 14 分钟
2025-06-22
GCC从2.9.3版本开始支持GCC手册的属性语法,后来一些编译器为了兼容以GCC为基础编 写的代码也纷纷支持了GCC的属性语法。 GCC的属性语法:
_attribute__((attribute-list))
GCC添加了一个扩展关键字__attribute__,这个关键字前后都有双下画线并且紧跟 着两对括号,用如此烦琐的语法作为说明符的目的一方面是防止入侵C++标准,另一方面是避免 和现有代码发生冲突。GCC的属性语法十分灵活,它能够用于结构体、类、联合类型、枚举类 型、变量或者函数。 前面介绍的设置对齐字节长度就是GCC的属性语法
#define PRINT_ALIGN(c, v) cout << "alignof(" #c ") = " << alignof(c) << ", alignof(" #v ") = " << alignof(v) << endl
__attribute__((aligned(16))) class X {int i;} a;
class __attribute__((aligned(16))) X1 {int i;} a1;
class X2 {int i;} __attribute__((aligned(16))) a2;
class X3 {int i;} a3 __attribute__((aligned(16)));
int main() {
PRINT_ALIGN(X, a);
PRINT_ALIGN(X1, a1);
PRINT_ALIGN(X2, a2);
PRINT_ALIGN(X3, a3);
}
根据__attribute__((aligned(16)))所在语句位置的不同,对类和对象的作用是不同 的。首先,放置在用户定义类型开始处的属性是声明类型的变量,而非类型本身,所以 __ attribute__ ((aligned(16))) class X { int i; } a;中对象a的对齐字节长度为16字节,而类X的对 齐字节长度为默认的4字节。 放置在class关键字或者整个类声明之后的属性声明的是类型 本身,一旦类型的对齐字节长度确定下来,其对象的对齐字节长度也就确定了下来,所以在 class__attribute__((aligned(16))) X1 { int i; } a1;和class X2 { int i; } __ attribute__ ((aligned (16))) a2;中类X1、X2以及对象a1、a2的对齐字节长度都是16字节。 类X3的对齐字节长度为默认的4字节。实际上属性描述的范围非常广, 除了刚刚提到的类和对象以外,对联合类型、函数等都可以进行声明,它还有属性覆盖和组合的规则
MSVC的属性语法
MSVC的属性语法和GCC相似,它引入了一个__declspec扩展关键字,不过这个关键字没有以 双下画线结尾,后面紧跟的是单对括号:
__declspec(attribute-list)
MSVC的属性语法规则简单多了
__declspec(dllimport) class X {} varX;
class __declspec(dllimport) X {};
将__declspec放置在声明对象语句的开头,则属性描述的是对象本身 varX,而不是类型X,如果没有声明对象,则忽略属性。而将__declspec放置在class和类型名之 间,描述的则是类型。
不管是GCC的属性语法还是MSVC的属性语法,它们都有一个共同的问题——属性声明过于 烦琐。为了解决这个问题,以及用标准化的方法统一属性说明符的语法规则,C++11发布了标准 的属性说明符语法。
标准属性说明符
C++11标准的属性表示方法是以双中括号开头、以反双中括号结尾,括号中是具体的属性
[[attr]]
[[attr1, attr2, attr3(args)]]
[[namespace::attr(args)]]
当需要多属性的时候可以在一个双中括号内用逗号分隔属性,也可以用多个双中括号来描述 不同的属性。属性本身还支持命名空间
C++11标准的属性说明符可用在C++程序中的几乎所 有位置,而且可用于几乎所有实体:类型、变量、函数、代码块等。 只不过不同的属性本身有特定的声明对象 比如[[noreturn]]
只能用于声明函数
在声明中,属性可出现在整个声明之前或直 接跟在被声明对象之后,在这种情况下它们将被组合起来。普遍的规则是,属性说明符总是声明 位于其之前的对象,而在整个声明之前的属性则会声明语句中所有声明的对象
[[attr1]] class [[attr2]] X {
int i;
} a, b[[attr3]];
attr1声明了对象a和b,attr2声明了类型X,X的属性也会影响到对象a和b, 最后attr3只声明了对象b。 虽然属性可以用于几乎所有的位置,不过到C++20为止, 绝大部分标准属性在声明中使用,目前只有fallthrough属性可以用于switch语句。
使用using打开属性的命名空间
为了防止不同编译器厂商在扩展属性的时候发生冲突,标准属性的语法支持了命名空间
[[gnu::always_inline, gnu::hot, gnu::const, nodiscard]]
//等同于[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]] [[nodiscard]]
inline int f();
GCC命名空间虽然保护了其属性不会受到其他属性的影响,但是为了声明这 些属性,程序员不得不重复指示命名空间,这造成了代码冗余。C++17标准对命名空间属性声明 做了优化,它引入了using关键字打开属性命名空间,随后即可直接使用命名空间的属性从而减少代码冗余 语法:
[[ using attribute-namespace : attribute-list ]]
其中attribute-namespace是命名空间的名称,attribute-list是命名空间内的属性,它们直接 使用冒号分隔,多属性之间使用逗号分隔。 使用using优化以上代码:
[[using gnu: always_inline, hot, const]][[nodiscard]]
inline int f();
将属性分为了两块,一块是标准属性nodiscard,另一块是带有GCC命名空 间的扩展属性always_inline、hot和const。使用新的语法不仅消除了命名空间的冗余问 题,而且很好地对属性进行了分类,让属性的修改和阅读都变得更加方便了。
C++17标准还规定,编译器应该忽略任何无法识别的属性。
标准属性
从C++11到C++20标准一共只定义了9种标准属性。
1. noreturn
noreturn是C++11标准引入的属性,该属性用于声明函数不会返回。 这里的所谓函数 不返回和函数返回类型为void不同,返回类型为void说明函数还是会返回到调用者,只不过没有返回值;而用noreturn属性声明的函数编译器会认为在这个函数中执行流会被中断,函数不会返回到其调用者。
例:
void foo() {}
void bar() {}
int main(){
foo();
bar();
}
在以上代码中foo函数的返回类型为void,但是没有指定noreturn属性,所以函数还是返回。 反汇编二进制程序可以得到汇编代码:
foo():
.LFB0:
push rbp
mov rbp, rsp
nop
pop rbp
ret
.LFE0:
bar() :
.LFB1:
push rbp
mov rbp, rsp
nop
pop rbp
ret
.LFE1:
main:
.LFB2:
push rbp
mov rbp, rsp
call foo()
call bar()
mov eax,0
pop rbp
ret
.LFE2:
在调用foo函数以后执行流会返回到main函数并且再调用bar函数,该流程没有中断。如果我们给foo函数添加noreturn属性,那么这个反汇编代码就会发生变化:
[[noreturn]] void foo() {}
void bar() {}
int main() {
foo();
bar();
}
生成的反汇编代码前面相同但main中的不同:
main:
.LFB2:
push rbp
mov rbp, rsp
call foo()
.LFE2:
对foo添加noreturn属性以后,main函数中编译器不再为 调用foo后面的过程生成代码了,它不仅忽略了对bar函数的调用,甚至干脆连main函数里的栈平衡 以及返回代码都忽略了。因为编译器被告知,调用foo函数之后程序的执行流会被中断,所以生成 的代码一定不会被执行,索性也不需要生成这些代码了。
2. carries_dependency
carries_dependency是C++11标准引入的属性,该属性允许跨函数传递内存依赖项 它通常用 于弱内存顺序架构平台上多线程程序的优化,避免编译器生成不必要的内存栅栏指令。所谓弱内 存顺序架构,简单来说是指在多核心的情况下,一个核心看到共享内存中的值的变化与另一个核 心写入它们的顺序不同。IBM的PowerPC就是这样的架构,而Intel和AMD的x86/64处理器系列则 并不属于此类。
该属性可以出现在两种情况中:
- 作为函数或者lambda表达式参数的属性出现,这种情况表示调用者不用担心内存顺序,函数内部会处理好这个问题,编译器可以不生成内存栅栏指令。
- 作为函数的属性出现,这种情况表示函数的返回值已经处理好内存顺序,不需要编译器在 函数返回前插入内存栅栏指令。
3. deprecated
deprecated是在C++14标准中引入的属性,带有此属性的实体被声明为弃用 虽然在代码中依然可以使用它们,但是并不鼓励这么做。当代码中出现带有弃用属性的实体时,编译器通常会给出警告而不是错误。
[[deprecated]] void foo() {}
class [[deprecated]] X {};
int main(){
X x;
foo();
}
函数foo和类X带有deprecated属性,所以在main函数被编译的时候,调用foo 以及实例化X的行为会被编译器警告。deprecated属性还能接受一个参数用来指示弃用的具体原因或者提示用户使用新的函数
[[deprecated("foo was deprecated, use bar instead")]] void foo() {}
void bar() {}
int main() {
foo();
}
在编译时会报出传入字符串参数的警告
deprecated这个属性的使用范围非常广泛,它不仅能用在类、结构体和函数上,在普 通变量、别名、联合体、枚举类型甚至命名空间上都可以使用。
4. fallthrough
fallthrough是C++17标准中引入的属性,该属性可以在switch语句的上下文中提示编译器直 落行为是有意的,并不需要给出警告。
void bar() {}
void foo(int a) {
switch (a) {
case 0:
break;
case 1:
bar();
[[fallthrough]];
case 2:
bar();
break;
default:
break;
}
}
int main() {
foo(1);
}
foo函数的switch语句里case 1到case 2存在着一个直落的行为,在有的编 译器中这种行为会给出警告提示,通过声明fallthrough属性可以消除该警告。 fallthrough属性必须出现在case或者default标签之前,否则会给出警告
5. nodiscard
nodiscard是在C++17标准中引入的属性,该属性声明函数的返回值不应该被舍弃,否则编译器给出警告提示。 nodiscard属性也可以声明在类或者枚举类型上,但是它对类或者枚举类型 本身并不起作用,只有当被声明为nodiscard属性的类或者枚举类型被当作函数返回值的时候才发挥作用
class [[nodiscard]] X {
};
[[nodiscard]] int foo() { return 1; }
X bar() { return X(); };
int main() {
X x;
foo();
bar();
}
函数foo带有nodiscard属性,所以在main函数中忽略foo函数的返回值会让编 译器发出警告。类X也被声明为nodiscard,不过该属性对类本身没有任何影响,编译器不会给出警 告。但是当类X作为bar函数的返回值时情况就不同了,这时候相当于声明了函数[[nodiscard]]
X bar()。在main函数中,忽略bar函数返回值的行为也会引发一个警告。 nodiscard属 性只适用于返回值类型的函数,对于返回引用的函数使用nodiscard属性是没有作用的
- 防止资源泄露,对于像malloc或者new这样的函数或者运算符,它们返回的内存指针是需要 及时释放的,可以使用nodiscard属性提示调用者不要忽略返回值。
- 对于工厂函数而言,真正有意义的是回返的对象而不是工厂函数,将nodiscard属性应用在 工厂函数中也可以提示调用者别忘了使用对象,否则程序什么也不会做。
- 对于返回值会影响程序运行流程的函数而言,nodiscard属性也是相当合适的,它告诉调用 方其返回值应该用于控制后续的流程。 从C++20标准开始,nodiscard属性支持将一个字符串字面量作为属性的参数,该字符串会包含在警告中,可以用于解释返回结果不应被忽略的理由
[[nodiscard("Memory leak!")]] char* foo() { return new char[100]; }
除了给出不该忽略返回值的理由外,也可以在信息中添加使用返回值的建议。对于库作者来说,这是一个非常实用的特性。
在C++20标准中,nodiscard属性还能用于构造函数,它会在类型构建临时对象的时候让编译器发出警告
class X {
public:
[[nodiscard]] X() {}
X(int a) {}
};
int main(){
X x;
X{};
X{ 42 };
}
它有两个构造函数,其中一个有nodicard属性[[nodiscard]]X() {}
,另一个则没有。表现在main函数中就是,因为X x;构造了非临时对象,所以不会有问题;而 X{}构造了临时对象,于是编译器给出忽略X::X()返回值的警告;X{ 42 };不会产生编译警告,因为 X(int a) {}没有nodicard属性。
6. maybe_unused
maybe_unused是在C++17标准中引入的属性,该属性声明实体可能不会被应用以消除编译器警告。 实际上,在一般环境中GCC、MSVC和CLang对于未使用的实例默认情况下都不会给出警告,除非有意设置了编译的相关参数,比如在GCC中添加-Wunused-parameter开关以打开对未使 用参数的警告(CLang也使用-Wunused-parameter,MSVC则是将警告等级调整到W4或以上)
int foo(int a, int b) {
return 5;
}
int main() {
foo(1, 2);
}
由于foo函数的形参a和b并未使用,因此在-Wunused- parameter开关的作用 下GCC给出未使用警告。要消除这种情况下的警告,可以对形参a和b添加maybe_unused属性
int foo(int a [[maybe_unused]], int b [[maybe_unused]]) {
return 5;
}
maybe_unused属性除作为函数形参属性外,还可以用在很多地方,比如类、结构体、 联合类型、枚举类型、函数、变量等
7. likely和unlikely
likely和unlikely是C++20标准引入的属性,两个属性都是声明在标签或者语句上的。 likely属性允许编译器对该属性所在的执行路径相对于其他执行路径进行优化;而unlikely属性恰相反。 通常,likely和unlikely被声明在switch语句
int f(int i) {
switch(i) {
case 1: return 1;
[[unlikely]] case 2: return 2;
}
return 3;
}
8. no_unique_address
no_unique_address是C++20标准引入的属性,该属性指示编译器该数据成员不需要唯一的地址,也就是说它不需要与其他非静态数据成员使用不同的地址。 该属性声明的对象必须是 非静态数据成员且不为位域
struct Empty {};
struct X {
int i;
Empty e;
};
int main() {
cout << "sizeof(X) = " << sizeof(X) << endl; //sizeof(X) = 8
cout << "X::i address = " << &((X*)0)->i << endl; //X::i address = 0
cout << "X::e address = " << &((X*)0)->e<<endl; //X::e address = 0x4
}
即使结构体Empty为空,但是在X中依然也占据了唯一地址。现在让我们给Empty e 添加no_unique_address属性
struct X {
int i;
[[no_unique_address ]]Empty e;
};
有了这个属性,编译器得知e不需要独立地址,于是将数据成员i和e编译在了同样的地址
sizeof(X) = 4
X::i address = 0
X::e address = 0
如果存在两个相同的类型且它们都具有no_unique_address属性,那么编译器不 会重复地将其堆在同一地址
struct Empty {};
struct X {
int i;
[[no_unique_address]] Empty e, e1;
};
int main(){
cout << "sizeof(X) = " << sizeof(X) << endl;
cout<< "X::i address = " << &((X*)0)->i << endl;
cout<< "X::e address = " << &((X*)0)->e << endl;
cout<< "X::e1 address = " << &((X*)0)->e1 << endl;
}
e和e1虽然都是带有no_unique_address属性的Empty类型,但是无法使用同一地址。 如果e 和e1不是同一类型,那么它们是可以共用同一地址的
struct Empty {};
struct Empty1 {};
struct X {
int i;
[[no_unique_address]] Empty e;
[[no_unique_address]] Empty1 e1;
};
输出结果:
sizeof(X) = 4 X::i address = 0 X::e address = 0 X::e1 address = 0
使用场景
无状态的类,这种类不需 要有数据成员,唯一需要做的就是实现一些必要的函数,常见的是STL中一些算法函数所需的函 数对象(仿函数)。而这种类作为数据成员加入其他类时,会占据独一无二的内存地址,实际上 这是没有必要的。所以,在C++20的环境下,我们可以使用no_unique_address属性,让其不需要 占用额外的内存地址空间。
贡献者
版权所有
版权归属:PinkDopeyBug