(C++) 属性说明符-标准属性

前言

cppref: 属性说明符序列(C++11 起) - cppreference.com

本文着重讲解属性说明符的标准属性

基础语法见:(C++) 属性说明符-语法

属性说明符是一种与具体编译器强绑定的内容。有的属性可以作为编译器的提示,有的则是直接作为设置。

在C++11之前,不同编译器拥有不同的语法,不同的说明符。在编写跨平台程序时非常麻烦,移植性一直不是很好。

到了C++11标准终于规定了属性说明符的标准。但是所支持的说明符随着标准的提出还不是很多。但该语法仍然允许使用编译器自身的说明符,为以后的扩展做了一定的预留。

标准属性

🏷️noreturn

⭐(C++11) 指示函数不返回

指示函数不返回。

void fun_0() {
}

[[noreturn]] void fun_1() {
}

int main() {
}

编译选项:-std=c++11 -O1

对应汇编 ,通过观察可以发现,没有标注[[noreturn]] 的函数还会会有对应的ret汇编。而标注了属性的函数则直接优化掉了。

fun_0():
        ret
fun_1():
main:
        movl    $0, %eax
        ret

🏷️carries_dependency

⭐(C++11) 指示在函数内外传播“释放-消费” std::memory_order 中的依赖链

指示释放消费 std::memory_order 中的依赖链传进和传出该函数,这允许编译器跳过不必要的内存栅栏指令。

对于本标签需要了解指令内存序的知识。

简单的说在多任务,多线程中为了保证资源内存访问的逻辑正确性,在生成对应的底层代码时会生成内存栅栏指令来维护正确性。

而这些生成的指令,肯定会消耗运行的效率。特别是在一些其实没必要保护的地方,就是多余的操作,该标签就是对应优化这种情况。

#include <atomic>
#include <cassert>
#include <iostream>

void print_0(int* val) {
    std::cout << __LINE__ << " " << *val << std::endl;
}

void print_1(int* val [[carries_dependency]]) {
    std::cout << __LINE__ << " " << *val << std::endl;
}

int main() {
    int x{8172252};

    // 注意,不同编译器
    // 不同cpp标准的情况下
    // 使用等号初始化和直接初始化之间的兼容性
    std::atomic<int*> p{&x};
    // 直接等号是最强的内存序seq_cst,这里选用较弱的consume
    int* local = p.load(std::memory_order_consume);
    assert(local);

    /**
     * 依赖是明确的,因而编译器已知 local 被解引用,
     * 且它必须保证保留依赖链,以避免(某些架构上的)一个内存栅栏。
     */
    std::cout << __LINE__ << " " << *local << std::endl;

    /**
     * print_0 的定义不透明(假定它未被内联),
     * 因而编译器必须发出一个内存栅栏,
     * 以确保在 print_0 中读取 *p 返回正确值。
     */
    print_0(local);

    /**
     * 编译器可以假定,虽然 print_1 也不透明,
     * 但指令流中仍保留了从形参到解引用值的依赖,
     * 且不需要(某些架构上的)内存栅栏。
     *
     * 显然,print_1 的定义必须确实保留这项依赖,因而此属性也会影响
     * 为 print_1 所生成的代码。
     */
    print_1(local);
}

本代码的汇编比较长,不直接展示了:对应汇编

🏷️deprecated

⭐(C++14) 指示以此属性声明的名字或实体,允许使用但因某种 原因 而不鼓励使用

指示声明有此属性的名字或实体被弃用,即允许但因故不鼓励使用。

deprecated 可以在使用时添加字符串字面量来辅助编程。

#include <iostream>

void fun_no_deprecated() {
    std::cout << __func__ << std::endl;
}

[[deprecated]]
void fun_0_deprecated() {
    std::cout << __func__ << std::endl;
}

[[deprecated("Use NeogenePeriod() instead.")]]
void fun_1_deprecated() {
    std::cout << __func__ << std::endl;
}

int main() {
    fun_no_deprecated();
    fun_0_deprecated();
    fun_1_deprecated();
}

在部分编译器,或者开启部分选项后可能会直接无法完成编译:

下面选用 gcc version 11.2.0 (x86_64-posix-seh-rev3, Built by MinGW-W64 project)的运行程序的输出:

main.cpp: In function 'int main()':
main.cpp:19:21: warning: 'void fun_0_deprecated()' is deprecated [-Wdeprecated-declarations]
   19 |     fun_0_deprecated();
      |     ~~~~~~~~~~~~~~~~^~
main.cpp:8:6: note: declared here
    8 | void fun_0_deprecated() {
      |      ^~~~~~~~~~~~~~~~
main.cpp:20:21: warning: 'void fun_1_deprecated()' is deprecated: Use NeogenePeriod() instead. [-Wdeprecated-declarations]
   20 |     fun_1_deprecated();
      |     ~~~~~~~~~~~~~~~~^~
main.cpp:13:6: note: declared here
   13 | void fun_1_deprecated() {
      |      ^~~~~~~~~~~~~~~~
fun_no_deprecated
fun_0_deprecated
fun_1_deprecated

🏷️fallthrough

⭐(C++17) 指示从前一 case 标号的直落是故意的,且会警告直落的编译器不应当对此诊断

指示从前一标号直落是有意的,而在发生直落时给出警告的编译器不应诊断它。

介绍一个switch的黑科技:(C/C++) 效率黑科技-Duff’s Device_duff device

void test(int n) {
    int cnt = 0;
    
    switch (n) {
    case 1:
    case 2:  // 直落时不警告
        cnt = 2;
    case 3:  // 编译器可在发生直落时警告
        cnt = 3;
        [[fallthrough]];  // 良构
    case 4:
        if (n < 3) {
            cnt = 4;
            [[fallthrough]];  // 良构
        } else {
            return;
        }
    case 5:
        while (false) {
            [[fallthrough]];  // 非良构:下一语句不是同一迭代的一部分
        }
    case 6:
        [[fallthrough]];  // 非良构:没有后继的 case 或 default 标号
    }
}

🏷️nodiscard

⭐(C++17/20) 鼓励编译器在返回值被丢弃时发出警告

若从并非转型到 void 的弃值表达式中,调用声明为 nodiscard 的函数,或调用按值返回声明为 nodiscard 的枚举或类的函数,则鼓励编译器发布警告。

/**
 * 给自定义类型指定
 */
struct [[nodiscard]] Node {};

Node test_self_type() {
    return {};
}

/**
 * (C++20 起)
 * nodiscard(字符串字面量)
 */
[[nodiscard("please do not discard")]]
int test_fun(int x, int y) {
    return x + y;
}

int main() {
    // 虽然函数没标注
    // 但是因为返回值是一个标注了的类型
    // 因此可能有警告
    test_self_type();

    // 可能有警告
    test_fun(1, 2);
    // 正常code
    int x = test_fun(3, 4);
    return x;
}

🏷️maybe_unused

⭐(C++17) 抑制对于未使用实体的编译器警告,如果有

抑制针对未使用实体的警告。

此属性可出现在下列实体的声明中:

对于声明为 [[maybe_unused]] 的实体,如果没有使用这些实体或它们的结构化绑定,那么编译器针对未使用实体发布的警告会被抑制。

#include <cassert>

/**
 * class/struct/union
 */
struct [[maybe_unused]] S {
    [[maybe_unused]] int x;
};

/**
 * enum
 */
enum [[maybe_unused]] E { A [[maybe_unused]], B [[maybe_unused]] = 42 };

/**
 * typedef,包括别名声明
 */
using Task [[maybe_unused]] = void (*)(void);

/**
 * 未使用函数 “f”
 * 未使用参数 “thing1” 与 “thing2”
 */
[[maybe_unused]]
void f([[maybe_unused]] bool thing1, [[maybe_unused]] bool thing2) {
/**
 * 未使用标签 “lb”
 */
[[maybe_unused]] lb:

    /**
     * 发行模式中,assert 在编译中被去掉,因而未使用 “b”
     * 无警告,因为它被声明为 [[maybe_unused]]
     */
    [[maybe_unused]] bool b = thing1 && thing2;
    assert(b);
}

int main() {
}

🏷️likely & unlikely

⭐(C++20) 指示编译器应当针对此种情况进行优化:通过某条语句的执行路径比其他任何执行路径更可能或不可能发生

允许编译器为包含该语句的执行路径,比任何其他不包含该语句的执行路径,更可能或更不可能的情况进行优化。

using ll = long long;

ll binPow(ll base, ll expo, ll mod) {
    ll ans = 1;
    base %= mod;
    if (expo == 0) [[unlikely]] {
        ans = 1 % mod;
    } else [[likely]] {
        while (expo) {
            if (expo & 1) {
                ans = ans * base % mod;
            }
            base = base * base % mod;
            expo >>= 1;
        }
    }
    return ans;
};

#include <iostream>

int main() {
    std::cout << binPow(2, 100, 1e9 + 7);
}

🏷️no_unique_address

⭐(C++20) 指示一个非静态数据成员不必具有与类中的其他所有非静态数据成员都不同的地址

允许此数据成员与其类的其他非静态数据成员或基类子对象重叠。

注意,一切以编译器实际情况为准!!!

#include <cstdint>
#include <iostream>

#ifdef _MSC_VER
#define no_unique_address msvc::no_unique_address
#endif  // _MSC_VER

/**
 * 空类
 * 任何空类类型对象的大小至少为 1
 */
struct Empty {};

/**
 * 按照默认方式对齐
 * 至少需要多一个字节以给 e 唯一地址
 */
void test_1() {
    struct X {
        int32_t i;
        Empty   e;
    };

    std::cout << "size = " << sizeof(X) << '\n';
}

/***
 * 优化掉空成员
 * 和前面的内存共用
 */
void test_2() {
    struct Y {
        int32_t                     i;
        [[no_unique_address]] Empty e;
    };
    std::cout << "size = " << sizeof(Y) << '\n';
}

/**
 * e1 与 e2 不能共享同一地址,
 * 因为它们拥有相同类型,
 * 尽管它们标记有 [[no_unique_address]]。
 * 然而,其中一者可以与 c 共享地址。
 */
void test_3() {
    struct Z {
        uint8_t                     c;
        [[no_unique_address]] Empty e1, e2;
    };

    std::cout << "size = " << sizeof(Z) << '\n';
}

/**
 * e1 与 e2 不能拥有同一地址,
 * 但它们之一能与 c[0] 共享,
 * 而另一者与 c[1] 共享
 */
void test_4() {
    struct W {
        uint8_t                     c[2];
        [[no_unique_address]] Empty e1, e2;
    };
    std::cout << "size = " << sizeof(W) << '\n';
}

int main() {
    // 任何空类类型对象的大小至少为 1
    static_assert(sizeof(Empty) >= 1);

    test_1();
    test_2();
    test_3();
    test_4();
}

🏷️assume(表达式)

⭐(C++23) 指示 表达式 在给定的位置永远为 true

指示表达式在给定位置总是求值为 true。

简单说,如果assume被通过为true,则下文的程序将可能被优化。否则行为未定义。

特别注意该表达式不会求值(但它依然会潜在求值)。

#include <cmath>

void g(int) {
}
void h() {
}

void f(int& x, int y) {
    /**
     * 编译器可以假设 x 是正数
     * 可以生成更高效的代码
     */
    [[assume(x > 0)]];
    g(x / 2);

    x     = 3;
    int z = x;

    /**
     * 编译器可以假设在调用 h 后 x 的值保持相同
     * 该假设本身不会调用 h
     */
    [[assume((h(), x == z))]];
    h();
    g(x);  // 编译器可以用 g(3); 替换该语句

    /**
     * 这里没有 assume
     * 或者说上面已经作用完上一个assume了
     * 编译器不能用 g(3); 替换该语句假设只在它出现的地方适用
     */
    h();
    g(x);

    /**
     * @brief
     * 编译器可以假设 g(z) 会返回
     * 根据上面和下面的假设,编译器可以用 g(10); 替换该语句
     */
    z = std::abs(y);
    [[assume((g(z), true))]];
    g(z);

    /**
     * 这里 y != -10 的情况下行为未定义
     */
    [[assume(y == -10)]];

    /**
     * 编译器可以用 g(5); 替换该语句
     */
    [[assume((x - 1) * 3 == 12)]];
    g(x);
}

int main() {
}

目前所支持的编译器还很少(目前仅gcc13, msvc19): 测试效果

END

关注我,学习更多C/C++,算法,计算机知识

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值