cpp笔记

(12 mins to read)

std::unordered_map

标准库都是用封闭寻址实现的(因为std标准要求迭代器递增以及删除都是均摊O(1)),而一般的高性能的哈希表都是开放寻址(flat_map)

  • extract:在不重新分配的情况下改变一个元素的key,只涉及指针的移动(auto nh = m.extract(2); nh.key() = 4; m.insert(std::move(nh));
  • bucket_count:桶的个数
  • max_load_factor:平均每个桶中的元素个数

std::ref

在传参时默认是拷贝,通过添加std::ref来使用引用。这在std::bindstd::thread以及模板(std::make_pairstd::make_tuple)等间接进行函数调用的地方有用。

std::reference_wrapper

std::ref产生的就是一个包含引用的std::reference_wrapper的对象。从而能够声明一个引用数组。

reference_wrapper<int> arr[] {x,y,z};

this = nullptr

这里类函数中没有使用任何类成员和类函数,也即与this的取值无关,通常会正常运行,但这仍然是未定义行为。

编译器其实会将类函数ABC::print()转化为一个全局的函数void _ABC_print(ABC *this)

编译器优化会假设程序不包含UB,因此会假设this != nullptr,从而进行某些优化,导致不符合预期。

1
2
3
4
5
6
7
8
9
10
11
class ABC{
public:
int a;
void print(){cout<<"hello"<<endl;}
};

int main(){
ABC *ptr = NULL:
ptr->print();
return 0;
}

delete this是合法的,只要该对象时动态分配的,并且之后不再使用

严格别名

-fstrict-aliasing

严格别名规则指编译器在看到多个别名时,如果它们不满足严格别名要求,就认为它们指向不同的内存区域,从而进行优化,导致产生与我们预期不符的代码。

严格别名要求(这是C语言的定义,相对简单一些):对象的值只能通过与该对象类型(1)兼容的类型及其CV限定、(2)有无符号版本及其CV限定、(3)成员中有该类型的union和聚合结构体、(4)字符类型(char, unsigned char, std::byte)访问。

注:这里的类型兼容是C语言的概念, 就是考虑typedef,数组退化指针后类型需要一致。

restrict是C语言的一个关键字,表明两个指针不会指向相同区域,但C++没有这个关键字。C++可以使用__restrict

正因为这个,reinterpret_cast<double*>(char*)在标准中其实是未定义的,因为此时用一个double指针去访问字符类型,违反了严格别名;除非在char*处存在一个double的对象(即需要用new(p) double创建一个double对象;而在C++20中,字符数组、内存分配、std::memcpy都会进行隐式对象创建(IOC),创建的类型在第一次访问时才确定)。

同样地,C语言的宏offset在C++里其实也是个未定义行为。

不过当前的编译器都能保证生成的代码是正确的。标准做法则是用memcpy来进行(构造一个对象,然后把这个对象memcpy到buffer中),而C++20引入了constexpr的bit_cast(比reinterpret_cast更安全)。

此外,C++17引入了std::launder,其用途是充当优化屏障,阻止常量传播、阻止去虚拟化分析以及阻止别名分析。

std::launder acts as an optimization barrier that prevents the optimizer from performing constant propagation.

std::launder用于改变一个对象的类型:

1
2
3
alignas(int) char data[sizeof(int)];
new(&data) int;
int *p = std::launder(reinterpret_cast<int*>(&data));

这里有两个case:

  1. 需要先用new构建int对象,当然在C++20后可以省去,因为有IOC(implicit object creation)
  2. 需要用launder洗一下指针,因为不能用旧指针(data*)去访问新的类型(int)

static_cast对指针类型的转换是很严格的,只能是相同类型的CV限定。

cpp-argument-passing

cpp-performace

初始化列表拷贝

std::initializer_list may be implemented as a pair of pointers or pointer and length.Copying a std::initializer_list does not copy the backing array of the corresponding initializer list.

注意初始化列表的拷贝是浅拷贝,可以把它当做是数组上的视图。

1
2
3
4
5
template <typename... Args>
auto f(Args... args) {
auto list = {args...};
return list; // 这里返回初始化列表使有问题的,生命周期错误
}

但是为了实现视图效果,编译器的实现方式通常是在调用函数前创建一个生命期足够长的基底数组,然后再基于该基底数组去构建所需的初始化列表,那么对于类对象,虽然说是一个视图,但是它会产生一次额外的非预期拷贝。

1
2
3
4
A a1, a2;
foo1({a1, a2});
// const A _a[2] = {a1, a2};
// foo(std::initializer_list<A>(_a, _a + 2));

伪析构函数

由于伪析构函数,所有标量类型都满足可析构要求,数组和引用类型则不满足。

1
2
template <typename T>
concept is_destruct = requires(T v) { v.~T(); };

一个好处是可以不需要考虑给定类型是否真的有析构函数。当然,即使是伪析构函数,调用后该变量的生命周期理论上应该是结束了,所以后续对其的操作均是未定义行为。

Opaque Pointer

不透明指针指代一个指向只有声明,但没有实现的类型的指针。

主要被用于向用户隐藏接口的具体实现,即pointer to impl范式。

头文件定义接口,源文件定义实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// interface (widget.h)
struct widget {
// public members
private:
struct impl; // forward declaration of the implementation class
// One implementation example: see below for other design options and trade-offs
std::experimental::propagate_const< // const-forwarding pointer wrapper
std::unique_ptr< // unique-ownership opaque pointer
impl>> pImpl; // to the forward-declared implementation class
};

// implementation (widget.cpp)
struct widget::impl {
// implementation details
};

std::string SSO

std::string的实现与std::vector类型,需要一个8字节的capacity,一个8字节的size,以及一个8字节的data。

而对于较短的字符串,可以考虑直接利用这24字节的栈空间进行存储,而不进行堆内存分配,即著名的短字符串优化(SSO)。

这里__min_cap=23,又由于C++11规定std::string必须以'\n'结尾,因此可用长度为22。对于小端序,__size_的最低位会被用于标记使用的是short还是long。

这其实很巧妙。如果是short,只需要利用高7位表示长度即可;而如果是long,只需保证__cap_是2的倍数即可,也即最后一位无关紧要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct __short
{
union
{
unsigned char __size_;
value_type __lx;
};
value_type [__min_cap];
};
struct __long
{
size_type __cap_;
size_type __size_;
pointer __data_;
};
union
{
__short __s_;
__long __l_;
};

NRVO和RVO

即命名返回值优化和返回值优化:编译器会直接在接收函数返回值的对象上进行单次构造,优化掉构造一个临时对象,再执行拷贝构造的过程。

事实上,编译器会将这个返回值改成一个函数参数,然后将返回值变成void。相当于先在函数外构造对象,然后再调用该函数操作这个对象。

显然,在return的时候加std::move只会弄巧成拙,导致编译器无法执行返回值优化。

clang

  • -S -emit-llvm:将中间代码表示LLVM IR写到.ll的文本文件中
  • -cc1 -emit-obj:生成对象文件,其中包含机器码,但尚未链接为可执行文件或动态链接库

void* arithmetic

默认情况下,void*指针是不允许进行算术操作的,因为不知道偏移量,只能(void*)((char*)p + 1)

不过可以添加-Wno-pointer-arith编译选项,此时(void*)p + 1会偏移一个字节。

aligned_storage

提供一段大小为size字节,对齐为alignment的存储空间。注意T必须是POD类型。

可以预分配,之后placement new(解耦内存分配和对象创建)。可以用来向量化读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<unsigned size, unsigned alignment>
struct aligned_storage
{
using type = struct { alignas(alignment) unsigned char data[size]; };
};


//std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
alignas(T) std::byte t_buff[sizeof(T)];

template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
}