C++标准演绎(未完)

作者 略游 q群 512 001 256

一、词汇定义

标准(standard):C++语言标准,在代码世界里,我们假设与公理等价。

结论:由标准推导出的事实。

规定:便于讨论,我们设定的一些规则。

类型(type):同一类型,它们在C++内存布局一致,使用同一代码段。用符号T表示。

实例(instance):基于类型的不同实例对象,在内存中拥有不同的值。

容器(container):容纳0到N个同类型的实例。用符号C表示,它也是一种类型。

函数(function):执行后,改变程序的内存状态,间接改变外部状态。用F表示。

命名:常见概念我们以特定名字表示。

二、内存(memory)

标准:内存最小单元是位(bit),1字节(byte)为8位。

标准:内存的大小是有限的。

规定:计算机在运行过程中,外部条件可能会导致电源断开、硬件错误、比特翻转。但我们规定代码世界不受理论之外事物的影响。

三、基础类型(base type)

标准

[std::numeric_limits<T>::lowest(), std::numeric_limits<T>::max()]为类型范围。

        注意,std::numeric_limits<float>::min()为最接近0的正浮点数,与std::numeric_limits<float>::max()的意义并不是相反。所以前区间应该是lowest,而不是min,这里标准库的命名是有一点问题的。

标准:std::numeric_limits<int>::min()与std::numeric_limits<float>::lowest()等价。

标准:float与double是不精确的、有限的、不满足交换律,类似实数的类型,称为浮点数。两个浮点数最小差值为:

std::numeric_limits<T>::epsilon()

标准:整型的表示是准确的,有以下类型表示不同大小:

int8_t、int16_t、int32_t、int64_t
uint8_t、uint16_t、uint32_t、uint64_t
以上类型,C++标准规定并不一定存在,取决于编译器,但我们假设(规定)它们一定存在。
int        表示非定长整数
unsigned   表示非定长自然数(非负整数)

标准:unsigned int 与 unsigned等价。

结论:int是有限整数。

结论:unsigned是有限自然数。

        float本意是浮点数、double是双精度浮点数。我们使用一个新的类型表示它们任意一个,叫实数(real)是不恰当的,因为它们表示的并不是数学上的实数,只是一种近似模拟。

命名:使用Float表示float或double。

三、一维类型

标准: 长度为N的 std::array、std::vector,下标访问区间为[0, C.size())。

命名: 长度为size,下标为index。

定义:下标使用size_t表示,它属于无符号整型。

定义:长度使用size_t表示,它属于无符号整型。

定义:一维点使用int表示,它属于有符号整型。

四、通用类型(common type)

标准: std::array<T, N>表示类型为T,长度为N的定长数组。

标准: std::vector<T>表示类型为T的变长数组。

定义: 二维点为std::array<T, 2>,三维点为std::array<T, 3>,N维点为std::array<T, N>。

命名: 我们使用以下命名表示通用类型,默认设定为2维的一个原因是,显示器为二维坐标系:

using Point = std::array<int, 2>;
using Position = std::array<Float, 2>;

         其中Point为整型点、Position为浮点数点。

命名:  通用类型默认为2维、有符号整型。若为x维度,则加上x后缀。无符号则加上U后缀。得出以下名字:

using Point3U = std::array<unsigned, 3>;
using Position4 = std::array<Float, 4>;

       当然,也会有以下定义:

using Point1 = std::array<int, 1>;
using Position1 = std::array<Float, 1>;

using Point0 = std::array<int, 0>;
using Position0 = std::array<Float, 0>;

         其中Point1与int基本等价,之所以定义Point1,是由于写容器算法时,容器的长度可以是[0, N]。故标准库也规定std::array<int, 0>是合法的,这是为了和vector、string等容器一致,可以存在为空的状态。

        另外在现实世界中,二次方程被三次方程求根公式兼容;圆是椭圆的特例;正方形是矩形的特例。前者均有更简单的公式算法,所以在程序中使用int表示一维,而不是Point1。比如二维向量与三维向量叉乘均有简洁的写法,4x4矩阵为了效率考虑不会使用NxN维矩阵的算法。

        从另一个角度讲计算机的性能是有限的,并不能保证编译器可以将Point1优化为int。

        另外在写代码层面,在访问Point1时,需要使用下标0访问,比如写x[0]。相比直接写x会麻烦很多。

五、迭代器(iterator)

标准:std::begin(C)为容器首元素。

标准:std::end(C)为容器尾元素之后的元素。

命名:迭代器为iter。

标准:迭代器自增指向容器下一个元素,最多到end。

结论:容器的所有元素在[std:begin(C), std::end(C))的区间内。

规定:从最普遍、最简易的角度,我们使用如下遍历方法:

for(auto& iter : c)
{
}

规定:通用类型由于长度固定,我们使用下标遍历。在需要使用下标时,也可以如下遍历:

T a;
for(std::size_t i = 0; i < std::size(a); ++i)
{
    a[i];
}

        在容器为固定类型时,可以使用 c.size()。如果 array<T, N>作为模板,使用N作为大小,以便编译器优化为常量表达式,如下:

template<typename T, size_t N>
constexpr void Zero(const std::array<T, N>& a)
{
    for(std::size_t i = 0; i < N; ++i)
         a[i] = 0;
}

六、通用类型运算(operator)

        以加法举例,有两个操作符,“+”和“+=”,以类型T举例,有如下写法:

T& operator+=(T& a, const T& b)
{
    a.x += b.x;
    a.y += b.y;
    return a;
}

T operator+(const T& a, const T& b)
{
    T ret = a;
    return ret += b;
}

         “+”的运算返回前操作数a的引用,而“+=”运算返回新的对象,为了避免重复代码出现,“+=”实现依赖于“+”。

        命名:返回值变量为ret。

        由上推广到通用类型,有以下运算:

//------------------------------N op N------------------------
template<typename T, std::size_t N>
constexpr std::array<T, N>& operator+=(std::array<T, N>& a, const std::array<T, N>& b)
{
	for (std::size_t i = 0; i < N; ++i)
		a[i] += b[i];
	return a;
}

template<typename T, std::size_t N>
constexpr std::array<T, N> operator+(const std::array<T, N>& a, const std::array<T, N>& b)
{
	std::array<T, N> ret = a;
	return ret += b;
}

        除了加法,还有减法、乘法、除法。实现以后我们便可如下计算:

constexpr Point a{100, 100};
constexpr Point b{200, 200};
constexpr Point c = a + b;    // c is 300, 300

        以上为N op N的操作,当b长度为1时,即N op 1的操作,如下:

//------------------------------N op 1------------------------
template<typename T, std::size_t N>
constexpr std::array<T, N>& operator+=(std::array<T, N>& a, const T& b)
{
	for (std::size_t i = 0; i < N; ++i)
		a[i] += b;
	return a;
}

template<typename T, std::size_t N>
constexpr std::array<T, N> operator+(const std::array<T, N>& a, const T& b)
{
	std::array<T, N> ret = a;
	return ret += b;
}

        实现以后支持如下计算:

constexpr Point a{100, 100};
constexpr Point b = a + 50;    // c is 150, 150

         对自身取负号,同理可得(为了写法统一,也实现取正号):

//------------------------------取负号------------------------
template<typename T, std::size_t N>
constexpr std::array<T, N> operator-(const std::array<T, N>& a)
{
    std::array<T, N> ret = a;
	for (std::size_t i = 0; i < N; ++i)
		ret[i] = -ret[i];
	return ret;
}

template<typename T, std::size_t N>
constexpr std::array<T, N> operator+(const std::array<T, N>& a)
{
    std::array<T, N> ret = a;
	for (std::size_t i = 0; i < N; ++i)
		ret[i] = +ret[i];
	return ret;
}

七、字面量后缀 

        以数字255举例,有以下字面量写法:

int a{ 255 };
unsigned b{ 255 };
double c { 255.0 };
float c {255.0f };

        由于我们使用 Float表示 float和 double,则需要自定义字面量f,由于避免和标准库冲突(即使标准库不存在f后缀),使用_f,故有以下定义:

inline constexpr Float operator""_f(long double r)
{
	return static_cast<Float>(r);
}
inline constexpr Float operator""_f(unsigned long long r)
{
	return static_cast<Float>(r);
}

        至此,我们可以使用字面量 100_f初始化一个 Float。

八、通用类型别名

        命名:时间使用FloatTime表示,基于float或double。

        命名:大小使用Size表示,它没有负数,所以基于unsigned。

        命名:矩形使用Rect表示。常用与窗口、图像区域,所以基于int。

        命名:三角形使用Triangle表示,与绘图相关,基于Position。

        命名:四边形使用Quad表示,与精灵相关,基于Position。

        命名:颜色使用Color表示,顺序为TODO,float精度足够。

        命令:分数使用Frac表示。需要存放分子分母,基于Point。

        以上均可以使用通用类型表示,使用using语法,如下:

using FloatTime = float;

using Size = PointU;
using Rect = Point4;

using Triangle = std::array<Position, 3>;
using Quad = std::array<Position, 4>;

using Color = std::array<float, 4>;

using Frac = Point;

        这时体现出定义通用类型的意义:

        1、所有算法通用,只需要写一遍。

        2、定义新类型,只需要一个using。

        3、命名不同的类型,但实际类型可能一致,避免转换操作。

九、通用类型转换(convert)

十、坐标系、方向、对齐(align)

        首先假设我们身处0维空间,此时整个世界是一个点,方向没有意义。

        如果我们身处1维空间,世界是一条线。以当前位置观察,产生两个方向,比如常见的我们用左右或上下来描述它。一般情况我们规定右为正,但当我们在南半球头朝下时,所规定的右变成了左。身处在1维世界,如果自身有朝向,则有前后的概念,则可规定前方即为正方向。

        规定:使用右手坐标系。

        规定:x、y、z、w为4维坐标轴简称。

        规定:2维坐标系,x正轴向右,y正轴向下( 基于显示器和书写习惯)。

        重回一维空间,规定自身点为原点(origin)。则自身位置可定义为0,正方向为+1,负方向为-1。数学上的欧氏空间有无穷远,往正轴走为正无穷,往负轴走负无穷。而代码世界的一切都是有限的,所以我们将坐标轴两端连起来,与0相对的位置称为max,如下图所示:

        命名:方向(direction)简写为dir。

        规定:dir类型为int。

        规定:dir取值为0、1、-1、std::numeric_limits<int>::lowest()。

        如此,要计算位移,我们有以下公式:

                x_1=x_0+dir*d

十一、缓冲区(Buffer)

        标准:std::byte表示字节类型。

        命名:缓冲区(Buffer)表示一段连续的、长度固定的内存。

        程序本身就像一张纸条,程序执行就像不断的操作纸条。内存就是纸条,使用std::array我们可以定义一块连续的内存。如果内存在堆上,那么就必须使用指针(pointer)指向它的首地址。在delete和free时我们不需要传入内存的长度(操作系统有记录长度),但平时我们需要使用长度时则需要手动记录。表示缓冲区我们经常会看到如下几种:

首地址长度
char*int
unsigned char*unsigned
void*size_t
uint8_t*

        上面两两组合就已经有12种表示方法。再加上std::string、std::vector<T>等就不知道有多少种方式了。所以我们做以下规定:

using Buffer = std::vector<std::byte>;
using BufferRef = std::span<std::byte>;
using BufferView = std::span<std::byte const>;

        命名:Ref表示引用,可读可写。

        命名:View表示视图,只读。 

十二、图像(image)

        说到图像,一般人都会想到它是2d的。

        规定:Image表示图像,2维。

        内存不存在2维的概念,但显示器显示2维,甚至3维,均是通过模拟的方式。所以我们应该如何实现2维内存块?如下:

        首先内存必须连续,以便获得较好的缓存(cache)命中率、减少内存碎片。

        有一种做法是,如下计算索引:

         index = y * w + x

        但这种方法需要一次乘法,所以我们额外记录行指针(row pointer),以快速访问。

        对于固定宽高的内存块,基于std::array,命名为Array2,实现如下:

template<typename T, size_t W, size_t H>
class Array2
{
private:
    std::array<T, W * H> _data;
    std::array<T*, H> _ptrRow;
}

        对于变化宽高的内存块,基于std::vector,命名为Vector2,实现如下:

template<typename T>
class Vector2
{
	std::vector<T> _data;
	std::size_t _w;// 宽
	std::vector<T*> _ptrRow; // 行指针
};

        由于32位颜色为图像格式主流,无论是8位灰度还是24位RGB,我们加载到内存后都统一为32位。所以有如下Image定义:

using Image = Vector2<uint32_t>;

         有的程序员认为可以使用如下进行2维数组访问:

std::array<std::array<T, W>, H> arr2d;
arr2d[y][x] = {};

         但实际上使用下标访问时,生成的汇编代码会有imov指令,做了一次乘法,类似于y * w + x的操作。

十三、平台(platform)

        C++标准是一种人为规定的理性。虽然非常的理性,但远远比不上数学和历史。比如1+1很难失效,发生过的事似乎永远不会逆转。但C++标准在不断发生变化,不过只有少数标准被废弃。

        C++代码会被编译器(compiler)编译为机器码,最终在CPU上执行。

        目前主流编译器有msvc、clang、gcc,主流CPU架构有x86、arm,操作系统有windows、linux、macos。所以事实上的C++标准的实现,是有多份的。在不同操作系统上,还提供了独自的api。这就使C++跨平台的说法成为了一种理论,实际上C++的跨平台就是相同的接口,在不同平台各写一份。基于这一点,我们需要一些实用的宏来判断操作系统,如下:

#define DL_PLATFORM_ERROR 0
#define DL_PLATFORM_WIN32 1
#define DL_PLATFORM_LINUX 2

#define DL_PLATFORM DL_PLATFORM_ERROR

#ifdef _WIN32
#undef	DL_PLATFORM
#define	DL_PLATFORM DL_PLATFORM_WIN32
#endif

#ifdef __linux__
#undef	DL_PLATFORM
#define	DL_PLATFORM DL_PLATFORM_LINUX
#endif

         判断当前程序是否64位:

//! 是否64位
consteval bool is_x64()
{
	return sizeof(void*) == 8;
}

        判断字节序是否小端:

//! 字节序是否小端
consteval bool is_little_endian()
{
	union
	{
		int a;
		char b;
	}num;
	num.a = 1;
	return !num.b;
}

十四、颜色(color)

Color

ColorRef

十四、日志(log)

十五、内存流

十六、文件系统

        C++虽然提供了filesystem,但它的不同平台实现细节差别很大,有必要规定一种通用、明确的文件系统规则。

        众所周知,windows的文件路径分割符为左斜杠,linux等为右斜杠。windows具有盘符,而linux等只有一个根目录“/”。

        规定:路径(path)表示文件(file)在存储介质的位置。

        规定:路径分隔符为右斜杠。

        通常在写一个路径时,如果文件不带后缀名,则无法区分是路径还是文件名。

XX、算法分类

        对容器每一个元素做同一操作:

namespace Every
{
    template<typename C, typename F>
    void Do(const C& c, F func)
    {
        for(auto& iter : c)
           func(iter);
    }
}
for(a
for(a

XX、标准库算法

sort

clamp

min

max

stable_sort

基础

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值