zoukankan      html  css  js  c++  java
  • (五)静态断言(下),static_assert

    二、静态断言与static_assert

    通过上一篇,我们可以看到,断言assert宏只有在程序运行的时候才能起作用。而#error值在编译器预处理时才能起作用。

    有时候,我们希望在编译时候能做一些断言。

    看下面这个例子:

    #include<iostream>
    #include<cassert>
    using namespace std;
    //枚举编译器对各种特性的支持,每个枚举值占一位
    enum FeatureSupports
    {                                 //为了阅读方便,以下注释后两位
        C99      =      0x0001,       // 0000 0001
        ExiInt   =      0x0002,       // 0000 0010
        SAssert  =      0x0004,       // 0000 0100
        NoExcept =      0x0008,       // 0000 1000
        SMAX     =      0x0010,       // 0001 0000
    };
    //一个编译器类型,包括名称、特性支持等
    struct Compiler
    {
        const char* name;
        int spp;                     //使用FeatureSupports 枚举
    };
    
    int main()
    {
        //检查枚举值是否完备
        assert((SMAX - 1) == (C99 | ExiInt | SAssert | NoExcept));     //检验设置的枚举是否有重位
    
        Compiler a = { "abc",(C99 | SAssert) };
        //....
        if (a.spp&C99)       //结果应该是C99
        {
            cout << "if判定中的结果为" << (a.spp&C99) << endl;
        }
    } 

    整个程序执行结果为    if判定中的结果为1           

    表明不会触发断言,说明定义的枚举没有冲突,并且执行情况与我们预期的一样,可能没看懂这个程序的意图,可以继续往下看。

    上述所示的是C代码中常见的“按位存储属性”的例子。

    在该例中,我们编写了一个枚举类型FeatureSupports,用于列举编译器对各种特性的支持。

    而结构体Compiler 则包含了一个int 类型成员spp。由于各种特性都具有“支持”和“不支持”两种状态,所以为了节省存储空间,我们让每个FeatureSupports 的枚举值占据一个特定的比特位置,并在使用时通过“或”运算压缩地存储在spp中。在使用的时候,则可以通过检查spp的某位来判断编译器对特性是否支持。

    比如:如果spp的值为5(即0000 0000 0000 0101),说明它支持C99和SAssert两种特性,因为spp的值是通过枚举量或运算得来的。

    但是,有的时候枚举值会非常多,而且还会在代码维护中不断增加。那么代码编写者必须想出办法对这些枚举进行校验,比如查验一下是否有重位等。在本例中程序员的做法是使用一个“最大枚举”SMAX,并通过比较SMAX-1与所有其他枚举的或运算值来验证是否有枚举值重位。

    可以想象,如果SAssert被误定义为0x0001,表达式(SAssert-1)==(C99 | ExtInt | SAssert | NoExcept)将不再成立。

    在本例中,我们使用了断言assert。但assert是一个运行时的断言,这意味着不运行程序我们将无法得知是否有枚举重位。在一些情况下,这是不可接受的,因为可能单次运行代码并不会调用到assert相关的代码路径。因此,这样的校验最好是在编译时期就能完成。

    相关的测试:

    如果有ExiInt的值改成和C99一样,其他都不变。

    我们编译一下:

    很明显,它不会出错。

    我们来执行一下:

    这时候,我们的断言才能检查出错误。

    在一些C++的模板的编写中,我们可能也会遇到相同的情况,比如下面这个例子:

    #include<cassert>
    #include<cstring>
    using namespace std;
    
    template <typename T,typename U>
    void bit_copy(T& a, U& b)
    {
        assert(sizeof b == sizeof a);
        memcpy(&a,&b,sizeof b)
    }
    
    int main()
    {
        int a = 0x2468;
        double b;
        bit_copy(a, b);
    }

    上述代码中assert是要保证a和b两种类型的长度一致,这样bit_copy才能够保证赋值操作不会遇到越界等问题。这里我们还是使用assert的这样的运行时断言,但如果bit_copy不被调用,我们将无法触发该断言。实际上,正确产生断言的时机应该在模板实例化的时候,即编译时期。

    (个人感觉这个例子不是很好,但是是这么个道理哈)

    上述代码同样编译通过,运行出错。不再测试。

    上述两个问题的解决方案是进行编译时期的断言,,即所谓的“静态断言”。事实上,利用该语言规则实现静态断言的讨论非常多,比较典型的实现是开源库Boost内置的BOOST_STATIC_ASSERT断言机制(利用sizeof操作符)。我们可以利用“除0"会导致编译器报错这个特性来实现静态断言。

    #define assert_static(e) 
      do { 
           enum{assert_static__ = 1/(e)}; 
       } while(0)

    在理解这段代码的时候,可以忽略do  while循环以及enum这些语法上的技巧。真正起作用的只是1/(e)。

     1 #include<iostream>           //不需要
     2 #include<cmath>              //不需要
     3 #include<cstring>
     4 using namespace std;
     5 
     6 #define assert_static(e) 
     7   do { 
     8        enum{ assert_static__ = 1/(e) }; 
     9    } while(0)
    10 
    11 template <typename T,typename U>
    12 void bit_copy(T& a, U& b)
    13 {
    14     assert_static(sizeof(b) == sizeof(a));     //如果改为!=则不会出错
    15     memcpy(&a, &b, sizeof b);
    16 }
    17 
    18 int main()
    19 {
    20     int a = 0x2468;
    21     double b = 2.1;
    22     bit_copy(a, b);
    23 }

    14行在编译的时候会报错。

    如果我们把14行改为 != 号则会通过编译,执行也没有问题,说明该错误确实是assert在编译时候发出的错误

    其实理解起来没什么困难,因为宏定义就要在编译的时候就会执行,所以,通过这个巧妙的方式正确打开我们 静态断言——static_assert !

    在C++11标准中,引入了static_assert 断言来解决之前的问题。

    static_assert用起来非常简单,它接受两个参数,一个是断言表达式,这个表达式通常返回一个bool值,另一个是警告信息,它通常是一段字符串。

    我们可以用static_assert 来替代上述代码的14行,如下:

        static_assert(sizeof(b) == sizeof(a), "the parameters of bit_copy must have same width");

    报错信息如下:(编译时期)

    这样的信息就非常清楚,也非常有利于我们排错。

    由于static_assert 是编译时期的断言,其使用范围不像assert一样受到限制。

    在通常情况下,static_assert 可以用于任何名字空间,例如:

    static_assert(sizeof(int) == 8, "This 64-bit machine should follow this!");
    int main() { }

    就这两行就可以,不需要其他的头文件或其他代码,第一行就可以执行。

    在C++中,函数则不可能像上述代码中的static_assert 这样独立于任何调用之外运行

    因此,将static_assert 写在函数体外通常是较好的选择,这让代码阅读者可以较容易发现static_assert 为断言而非用户定义的函数。

    而反过来讲,必须注意的是static_assert 的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式

    如果使用了变量,就会导致错误。例如:

    int positive(const int& n)
    {
        static_assert(n > 0, "value must > 0");
    }

    上述代码使用了参数变量n(虽然是一个const参数),因而无法通过编译。对于此例,如果需要的只是运行时的检查(为什么这么说呢,变量n一定是运行的时候才能通过传递参数得知,所以,从代码的意图上看可能需要的只是运行时的检查),那么还是应该使用assert 宏。

    三、总结

    到这里,我们的断言机制基本上就完了,不同的机制对应不同的类型,不同的时期。

    运行时期的  assert 宏

    编译时期的 static_assert 静态断言

    预处理时期的 #if 和 #error 机制

    希望大家能够正确运用它们。

    感谢您的阅读,生活愉快~

  • 相关阅读:
    我对JavaWeb中中文URL编码的简单总结
    URL的编码和解码
    Maven警告解决:Using platform encoding (UTF-8 actually)
    JavaWeb编码浅解
    pageContext对象的用法详述
    JspSmartUpload 简略使用
    Web开发相关笔记 #05# MySQL中文无法匹配
    Web开发相关笔记 #04# WebSocket
    Eloquent JavaScript #02# program_structure
    Eloquent JavaScript #01# values
  • 原文地址:https://www.cnblogs.com/lv-anchoret/p/8414456.html
Copyright © 2011-2022 走看看