模板规则推导

模板推导规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template<typename T>
void passByRefFun(T& val) { }

template<typename T>
void passByUniRefFun(T&& val) { }

template<typename T>
void passByValueFun(T val) { }

void fun3(int& a) { }
void fun3(int&& a) { }

void test()
{
int a = 0; //a int
const int b = a; //b const int
const int& c = a; //c const int&

passByRefFun(a); // int&
passByRefFun(b); // const int&
passByRefFun(c); // const int&
//passByRefFun(27); // 报错

passByUniRefFun(a); // int&
passByUniRefFun(b); // const int&
passByUniRefFun(c); // const int&
passByUniRefFun(27); // int&&

passByValueFun(a); // int
passByValueFun(b); // int
passByValueFun(c); // int
passByValueFun(27); // int

{
int&& a = 10;
fun3(a); // called void fun3(int& a)
fun3(std::move(a)); // called void fun3(int&& a)

}
}

SFINAE

SFINAE(Substitution Failure Is Not An Error)是 C++ 中的一个重要概念,主要用于模板编程。它的意思是,当模板参数替换导致错误时,编译器不会将其视为错误,而是会继续查找其他可能的匹配。这使得我们可以在模板中进行条件编译,选择合适的重载或特化

SFINAE 的优势
条件编译:可以根据类型特征选择不同的实现,增强代码的灵活性和可重用性。
避免编译错误:当模板参数替换导致错误时,编译器不会将其视为错误,而是继续查找其他匹配的模板。
类型安全:通过类型特征,可以确保只有符合条件的类型才能使用特定的模板实现。

SFINAE 是 C++ 模板编程中的一个强大工具,允许开发者根据类型特征选择合适的模板特化或重载。通过结合 std::enable_if 和类型特征,开发者可以编写更灵活和安全的代码。

电子书目录

渲染整理

开放世界的场景管理

1. 切成tile
2. 数据分级LOD
3. 根据天顶角,调整远平面,裁剪数据

游戏引擎分层架构

1. EditorLayer
2. FuntionLayer
    1. Rendering
    2. Animation
    3. Camera
    4. Physics
    5. Script
3. ResourceLayer
4. CoreLayer
    1. threadPoolManagement
    2. memoryPool
    3. mathLibrary
5. PlatformLayer
    1. RHI

Renderable

1. shader
    1. vs,fs
    2. macro
2. RenderState
3. MVP 
4. Texture
5. DrawImp

渲染效果

1. 前向渲染
      1. PBR
      2. 布林冯模型
2. 阴影shadowMap:
    1. 根据相机的位置,对整个场景,绘制出深度图,表示光的可见性
    2. 渲染时,将相机位置变化到光源位置,计算深度值与shadowmap进行比较,从而决定绘制的亮度
3. 楼的倒影
    1. 矩阵楼块插入地面
    2. 模板测试,只有地面和水才绘制
4. AO
5. 聚光灯效果
6. UV动画

技术点提炼

1. 楼块
    1. 切成小块
    2. 柔化圆角
    3. 贴UV
2. 3d瓦片绘制
    1. 瓦片的加载,与cache
    2. 非实施例渲染,按材质进行分类渲染
    3. 实例化渲染,动态计算lod进行渲染
3. 模型渲染
    1. PBR渲染
    2. 非PBR渲染

c++ 可变参数模板

可变模版参数(variadic templates)

可以对参数进行高度泛化,标识0到任意个数参数

两种展开形式

  1. 使用特化的终止函数结合递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

//终止函数
int multiply()
{
return 1;
}

//递归展开
template <typename T, typename ...Args>
int multiply(T&& t, Args&& ...arg)
{
return t * multiply(arg...);
}

int main(int argc, const char * argv[]) {

cout << multiply(3, 4, 5) << endl;

cout << multiply() << endl;

return 0;
}
  1. 使用逗号表达式
    初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class F, class... Args>void expand(const F& f, Args&&...args)
{
//这里用到了完美转发,关于完美转发,读者可以参考笔者在上一期程序员中的文章《通过4行代码看右值引用》
initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}

int main(int argc, const char * argv[]) {

expand([](int i) {
cout<<i<<endl;
}, 1,2,3);
return 0;
}

initializer_list

为了编写处理不同数量实参的函数,如果参数类型相同,可以使用initializer_list, 如果实参类型不同,使用可变参数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct myclass {
myclass(int,int)
{
cout << "myclass constructor" << endl;
}
myclass(initializer_list<int>)
{
cout << "myclass initializer_list constructor" << endl;
}
};


int main(int argc, const char * argv[]) {

auto il = {10, 20, 30};

myclass foo {10,20}; // calls initializer_list constructor
myclass bar (10,20); // calls first constructo

return 0;
}

c++继承权限

  1. 继承有三种权限,public,proteced,private,默认不写是private

  2. 权限的最低是public,其次是protected,最高private

  3. 继承方式代表是父类属性在当前类中的最低呈现

  4. 父类中的privated属性在子类中不可访问

OpenGL模板测试流程

模板测试流程

不考虑earlyZ的情况下,fragment执行后,进行模板测试,通过后,进入深度测试

模板测试一般使用流程:

1. 启用模板缓冲写入
2. 渲染物体,更新模板缓冲
3. 禁用模板缓冲写入
4. 渲染其他物体,根据模板缓冲内容决定是否丢弃片段
使用模板测试绘制物体轮廓的例子
1
2
3
glStencilMask();
glStencilFunc(GLenum func, GLint ref, GLuint mask);
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
1. 开启模板测试和深度测试 2. 第一次render pass,主要是绘制,并写入模板 1. 开启模板测试和深度测试 2. glStencilMask(0xFF); 3. glStencilOp(keep, keep, replace); 4. glStencilFunc(always, 1, 0xFF); 5. 绘制物体 3. 第二次render pass, 放大物体,通过模板测试剔除非边缘像素 1. 将物体缩放变大 2. 关闭深度测试 //因为这里的边缘不需要有拓扑关系 3. 关闭模板写入glStencilMask(0x00); 4. glStencilFunc(not_equal, 1, 0xFF); 5. 绘制物体

关于OpenGL里面的Mask

1. 写入颜色是,r,g,b,a 分别与对应的mask,进行&运算后写入
2. depth也是同样道理,如果设置成true,就是允许写入,设置成false,不允许写入
3. stencil的Mask,是0xFF~0x00,之间的256个数,一般设置是0xFF,允许任意值写入,0x00是不允许写入

实现完美转发

什么是完美转发?

在理解什么是完美转发之前,需要知道什么是万能引用?

在模板推导过程中,使用T&& a,这时候,并不是类型T的右值引用,而是万能引用,如果a是左值,这时候,就是一个左值引用,如果a是右值,这时候就是一个右值引用,具体原理是发生引用折叠。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
void Add(T&& a, T&& b) {
cout << a << endl;
cout << b << endl;
}

int main() {

Add(4, 5); // a,b的类型会被推导成int&&
int a = 0;
int b = 0;
Add(a, b); // a,b的类型会被推导成int&

return 0;
}

根据参数的具体类型,来实例化模板,准确的生成左值引用和右值引用的实例,这就是万能引用

万能引用遇到的问题?

上面的例子中,Add函数参数虽然是类型是右值引用,但是值确实左值,导致函数内继续使用调用其他函数时,参数类型由右值变成左值,也就是无法将右值引用这个类型继续转发.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename T>
void AddImp(T&& a, T&& b) {
cout << a << endl;
cout << b << endl;
}

template <typename T>
void Add(T&& a, T&& b) {
AddImp(a, b);
}

int main() {

Add(4, 5);
int a = 0;
int b = 0;
Add(a, b);

return 0;
}

解决方案: std::forward

1
2
3
4
5
template <typename T>
void Add(T&& a, T&& b) {
AddImp(std::forward<T>(a), std::forward<T>(b));
}

std::forward的具体实现

1
2
3
4
5
template <class _Tp>
_Tp&& forward(typename remove_reference<_Tp>::type& __t)
{
return static_cast<_Tp&&>(__t);
}

具体分析一下,也是通过引用折叠来实现

  1. 如果_Tp的类型是int&, 通过引用折叠 int& && 折叠后是左值引用int&
  2. 如果_Tp的类型是int&&, 通过引用折叠 int&& && 折叠后是int&&

c++ 11 智能指针

智能指针

share_ptr使用

sharet_ptr构造函数和std::make_share 的区别

  1. 两个堆内存和一个堆内存,std::make_share效率更高

weak_ptr使用

  1. expired(),返回指向对堆对象是否释放
  2. use_count,share_ptr的强引用计数
  3. lock,返回share_ptr,如果释放,返回空

share_ptr线程安全话题

  1. share_ptr引用计数本身是线程安全的
  2. 一个share_ptr对象,在多个线程操作,不能保证线程安全
  3. share_ptr指向的对象本身,进行操作时,也无法保证线程安全,完全取决于指向对象是否线程安全

stl容器多线程安全时的性能考虑

code使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int main()
{
shared_ptr<Person> person1;

shared_ptr<Person> person2(nullptr);

shared_ptr<Person> person3(new Person(10));

shared_ptr<Person> person4 = std::make_shared<Person>(5); //效率更高,内存分布在一起

shared_ptr<Person> person5(std::move(person3)); // person3无法再使用

shared_ptr<Person> arary(new Person[10], deletePersonArray);

weak_ptr<Person> weak_Person = person5;

cout << weak_Person.use_count() << endl;

shared_ptr<Person> person6 = person5;

cout << weak_Person.use_count() << endl;

person5.reset();

cout << weak_Person.use_count() << endl;

person6.reset();

if (weak_Person.expired()) {
cout << weak_Person.use_count() << endl;

auto shareptr = weak_Person.lock();

cout << shareptr << endl;
}

return 0;

}

c++ 右值引用

1. 什么是右值?

有名称,可以取地址的值,是左值。
没有名称,不能取地址的值,就是右值,另外类似函数返回值这种临时变量,定义为将亡值,也是右值。
c++11中,所有的值,必属于左值,将亡值,和纯右值。

2. 左值引用,右值引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {

int a = 0; //ok

int& b = a; //ok,左值引用
int& c = 0; // not ok,左值引用无法引用右值

const int& d = 0; // ok,常左值引用,可以绑定右值
const int& e = a; // ok,常左值引用,可以绑定左值

int&& f = 0;//ok 右值引用绑定右值
const int && g = 0; // ok,常右值引用可以绑定右值
//但是实际上没有意义,因为绑定的右值无法修改,一般右值引用是为了实现移动语义,降低copy消耗

int&& h = a;//not ok,右值引用无法绑定左值

return 0;
}

左值引用,只能绑定左值
常左值引用,可以绑定常量左值,右值,非常量左值和右值
右值引用,只能绑定非常量右值
常右值引用,可以绑定常量右值,非常量右值

3. 讨论右值引用,要注意排除返回值优化

如果关闭返回值优化,可以参考
https://www.yhspy.com/2019/09/01/C-%E7%BC%96%E8%AF%91%E5%99%A8%E4%BC%98%E5%8C%96%E4%B9%8B-RVO-%E4%B8%8E-NRVO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Person GetPerson(){
return Person();
}

int main()
{
Person person = GetPerson();
person.print();

/*
一共执行三次构造
1 Person()默认构造函数
2 GetPerson函数返回时,生成临时对象,调用移动构造函数
3 使用临时对象,构造person,调用移动构造函数
*/
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Person&& GetPerson(){
return Person();
}

int main()
{
Person person = GetPerson();
person.print();

/*
一共执行两次次构造,这种写法是错的,会有warnning
Returning reference to local temporary object
1 Person()默认构造函数
2 右值引用,引用了已经析构的临时对象
3 使用临时对象,构造person,调用移动构造函数
*/
return 0;
}

4. 函数返回值,如果没有写左值引用,就是临时变量属于右值

1
2
3
4
5
6
7
8
9
10
11
12
Person GetPerson(){
return Person();
}

int main() {

Person person1 = GetPerson(); //调用一次构造,两次次移动构造

Person&& person2 = GetPerson(); //调用一次构造,一次移动构造

return 0;
}

理解上面person1和person2的区别,person1是根据临时变量构造了一个新的对象
person2是直接对临时变量的右值引用

注意

1
2
3
4
5
6
7
const Person& GetPerson1(){
return Person();
}

Person&& GetPerson2(){
return Person();
}

上面两种写法都是错误的,返回的是临时变量的引用,可以编译通过,但是有警告

Returning reference to local temporary object