SomeCpp

Last Updated on 2020年12月10日

零碎知识点

  • fflush仅仅是为了输出而设计的, 标准中并没有说明它对输入缓冲的效果.
  • 一元运算符和=是右结合的,这和<<是完全不同的,a=b=c意味着a=(b=c),b=c将先执行
  • C++11后引入了thread_local型的生命周期,这种对象和线程的生命周期是一致的
  • C++11中很多类型都支持列表初始化,a = {value1,value2...}
    • 列表初始化会额外检测是否可能存在信息丢失,如果可能存在丢失,则无法通过编译期检查.
    • 对于自定义类,支持列表初始化意味着存在这样的ClassName(initializer_list<T> lst)构造函数
    • 只要对象支持使用{}构造/赋值,相应的场合都可以使用{},例如return {1,2,3}
  • 使用默认初始化时一定不要加括号,ClassName var()并不是在进行默认初始化,而是一个函数声明.
  • char在参与计算时,具体是有符号/无符号是由编译器决定的.
  • 从C语言继承而来的标准库<name.h>都有其<cname>版本,目前并没有发现使用<cname>的特殊优势
    • #include <cname>后,std::空间内一定有对应的函数名,全局空间可能也有
    • #include <name.h>后,全局空间内一定有对应的函数名,std::空间可能也有
  • 优先使用enum class
    • enum class定义的枚举是强类型的,只能用Color c=Color::red这种表达式,enum则是弱类型的,Light c=0;int d=Light::red这种表达式都是合法的
    • 另外,enum应当出现在头文件中,而不是源代码中
  • 使用usigned int来表示大正数是有瑕疵的,因为代码中往往充斥着类型转换.usigned int类型更适合管理纯粹的二进制数据,这些数据的值意义更加淡化
    • 如果你保持总是使用有符号数,那么就永远不会碰到符号转换引起的错误,可惜的是,size_t这样的历史错误已经无处不在了.
  • 数值字面值对应的默认类型是依赖于编译器的,一定要注意
    • 例如在VS中,字面值常量默认是int型的 long long a = 2^40,看起来没有问题,其实等价为int tmp=2^40; long long a = tmp
  • C++11起,提供了一个std::to_string()std::stoi()来实现数值和字符串之间的转换.
  • 在C++11中:
    • 整数除法规定为截断,即向0取整,总是删除小数部分.早期版本中则由编译器决定取整的方式.
    • 整数取模m%n的结果,其结果的绝对值为abs(m)%abs(n),符号和m相同
  • 函数形参的默认值可以是”变量”,例如void bar(int x=a);这一特性十分危险,一定要慎重使用.
    • 函数形参的默认值最好保持为常量.
  • 所有控制流记号后面都必须跟语句,如果什么也不做,就用;空语句
  • switch后的{}内是一个作用域,case的作用和goto的label相同,并不分割作用域
  • (condition?exprT:exprF)条件表达式仅当exprTexprF都是左值表达式时才返回左值.
  • (expr1,expr2)逗号表达式顺序执行,返回最后一个表达式,这里就是expr2
  • sizeof(EXPR)是一个constexpr,完全在编译期求值.
  • 优先级和结合律并不能处理所有的计算顺序问题,这主要是为编译器留下一定的优化空间.
    • 例如(expr1) opt (expr2),我们可以确定expr1expr2计算完之后才开始opt,但是expr1,expr2具体是哪个先执行,是由编译器决定的.
    • 典型例子: cout<<i<<++i<<endl可以化为cout<<i<<(++i)<<endl,再从左向右结合,得((cout<<i)<<(++i))<<endl可以看出,这里(cout<<i)<<(++i)(++i)(cout<<i)的求值顺序就是不确定的.
  • C语言中,只使用static inline. 这样可以保持和C++一样的语义和用法.(https://stackoverflow.com/questions/216510/extern-inline)
  • 长度为0的”匿名位域”用于声明:占满存储单元的剩余空间 ,从而使得之后的位域从新的存储单元开始.

涉及模板时的名字查找:

  • 定义
    • Unqualified name : 简单的标识符,不涉及::,->,.
    • Qualified name: 与上面相反,例如a.b,A::b,p_a->b,这里,b都是Qualified name.
  • Dependent name: 对于一个 Qualified name, 如果对于一个名字的解析依赖了非concrete的类型,那么就称之为dependent name
    • 标准规定: Unqualified name 一定不是 dependent name. (这主要是为了避免写出冗杂的代码. )
  • 模板的2-phase名字查找
    • Undependent name 一定是在模板定义的位置解析及绑定的. 称之为phase 1
    • Dependent name 一定是在模板示例化的位置解析及绑定的. 称之为phase 2
// x的名字解析
// 1. x 是unqualified name,进一步,一定是undependent name, 所以一定是在 phase 1 绑定的
// 2. x的查找一定不会进入Bar<T>,因为在Foo<>的定义阶段,Bar<T>仍然是个未知类型.换言之,对x的查找顺序是:foo的局部作用域-> Foo::作用域 -> 外部作用域, Bar<T>被跳过了
// 3. 注意,上面的流程是因为 x 被标准限制了, x只能是 undependent name.
// y的名字解析
// 1. 对于this->y, y是一个 qualified name ,进一步的, y的值依赖于this,而this不是concrete的,所以y是一个dependent name, 是在phase 2 绑定的
// 2. 相比于x,这里对y的查找会额外进入`Bar<T>`,因为在模板实例化阶段,`Bar<T>`已经是一个concrete的类型了.
template<class T>
struct Bar{
int x=0;
int y=1;
};
template<class T>
class Foo: public Bar<T>{
    int foo(){return x;} // error
    int bar(){return this->y;}// good
};

补充: dependent name的语法歧义

  • 对dependent name的解析有两种情况会出现语法歧义, 此时可能需要用户帮助编译器解决歧义.
  • 默认情况下,当B是一个dependent name时, A::B,a.B,pa->B总是被解释为一个值, 假设记作v_foo
    • A::B * a被解释为v_foo * a
    • A::B<x>a.B<x>pa->B<x>被解释为(v_foo < x) >,这将导致一个语法错误, >运算符的右侧操作数缺失了.
      • 相应的A::B<x>(y)就不会报错了,因为(v_foo < x) > (y)是一个合法的表达式
  • 必要时,需要通过typenametemplate帮助编译器解决歧义.
    • 例如typename A::B * a创建了一个指针,指向A::B类型
    • 例如A::template B<x>(y),产生了一个函数调用,调用了A::内的一个模板函数.

C 中的 Struct hack

  • 如果一个struct X的内存布局中最后一个类型是数组,那么我们可以通过malloc(sizeof(struct X) + sizeof(int) *(N - 1))的方式为实例额外分配更大的空间,使得尾部的数组可以”合法”的越界,形成一定的”动态size”的感觉.struct hack的方案比指针作为成员要简单一些,当指针作为成员时,你不可避免的需要做两次malloc,以及对应的两次free
struct Array{
  int N;
  int v[1]
};

volatile

  • volatile用于指示编译器”变量可能会被编译器不可见的方式修改”, 例如,其地址会被其他库拿到,然后修改.
    • 指针也可以声明为volatile T * p的,用于表明指向的地址可能会被编译器不可见的方式修改.
  • volatile一般意味着:
    • 生成指令时,会绕开所有级别的缓存(L0/L1/L2等).
    • 上面的约束也就导致部分编译期优化可能会被禁用.
  • volatile的行为随机器而改变,其用法和const相同,但更加严格,不允许T转换到volatile T也不允许T绑定到volatile T&&

库开发中在.h中暴露API的合理方式:

  • 核心思想: 暴露出的structclass都必须保证不依赖库内对象的内存结构,只能有symbol级别的依赖,不能有地址偏移的依赖.
  • 提供两组接口,xx.h仅暴露C风格接口,xx.hpp仅暴露C++风格的接口,前者一般是后者的一个wrap.
  • 头文件中只能使用Opaque类型,也就是说,除了内置类型,只能使用MyStruct *MyClass *
  • 暴露出的类一定是一个普通类,不能带有虚函数表.
    • 用户编译时产生的代码如果使用了虚函数,那么这次编译就会依赖库中的虚函数表.库开发者对虚函数表的任意改动都会引入兼容性问题.
  • 暴露出的接口中,不能包含任何编译器自动生成的内容,主要是合成构造函数/合成析构函数/合成拷贝控制.
    • 这些函数在实现时必须是类外定义.
    • 因为客户的编译器合成出的版本极有可能和你的编译器合成的版本不一致.