C++零碎笔记
Dionysen

遇到的细碎的问题及其解决方法。

函数参数传递

pass by value: 在栈区开辟了形参的内存空间,并调用拷贝构造函数把实参复制给形参,如果资源较大,拷贝消耗较大。

pass by reference: 本质上是传递指针,通过指针间接寻址。

注意:当传递内置类型时,如int、char,指针占用的内存空间高于变量所占用的内存空间。而寻址会降低程序的效率,应当使用值传递并使用std::move。传递STL容器时也可以使用值传递加移动语句。

函数返回值

在现代 C++ 中,返回临时对象实际上是可以的,因为编译器会进行返回值优化 (RVO - Return Value Optimization) 和移动语义优化。

  1. 安全的返回方式:
// 完全可以这样写
std::string getName() {
std::string name = "test";
return name; // 编译器会优化,不会有额外复制
}

// vector 也可以直接返回
std::vector<int> getNumbers() {
std::vector<int> nums = {1, 2, 3};
return nums; // 同样会被优化
}
  1. 不安全的返回方式:
// 危险!返回局部变量的指针或引用
int* getValuePtr() {
int value = 42;
return &value; // 错误:返回栈上变量的地址
}

// 危险!返回局部变量的引用
int& getValueRef() {
int value = 42;
return value; // 错误:返回栈上变量的引用
}
  1. 关于对象返回的一般规则:
  • 返回值类型的对象是安全的(依赖 RVO 和移动语义)
  • 不要返回局部变量的指针或引用
  • 如果对象很大,考虑使用引用参数或智能指针
  • 如果是自定义类,确保实现了移动构造函数和移动赋值运算符
  1. 性能考虑:
// 现代C++中这样写很好
std::vector<int> createVector() {
return std::vector<int>{1, 2, 3}; // 编译器会优化
}

// 如果对象很大或构造成本高,可以用引用参数
void createLargeObject(LargeObject& out) {
// 直接修改 out
}

总结:

  1. 返回临时对象在现代 C++ 中是安全的
  2. 但要避免返回指向局部变量的指针或引用
  3. 对于大对象或特殊情况,可以考虑使用引用参数
  4. 确保你的类支持移动语义(如果需要的话)

如果没有移动构造函数,返回一个临时对象会再调用时创建一个新的对象(调用拷贝构造函数),如果没有手动实现的拷贝构造函数,浅拷贝可能出现内存泄漏。

不考虑代码设计问题,最安全的方式是传入需要修改的对象引用,直接对对象进行修改。

OpenGL手动控制帧率

通过计算渲染当前帧所消耗的时间,对比目标帧率时一帧应该消耗的时间,如果渲染当前帧所需时间更短,则让当前线程休眠目标帧率应消耗时间与当前帧渲染时间之差。

auto framerate = 120;

// main loop 中
auto time = Time::GetTime();
Timestep timestep = time - m_LastFrameTime;
m_LastFrameTime = time;

auto sleepDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::duration<float>(1.0 / framerate - timestep.GetSeconds()));

if (1.0 / framerate > timestep.GetSeconds())
std::this_thread::sleep_for(sleepDuration);

但事实上这样会导致帧率的不稳定,目前还没找到更好的办法。

降低帧率最好的方法仍然是开启VSync。

warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失

一般是使用中文字符集编写的源代码,使用utf-8编码打开,会出现乱码,编译选项中添加/utf-8即可。

xmake.lua

add_cxxflags("/utf-8")

参数包(parameter pack)

参数包是 C++11 中引入的一个特性,允许模板接受任意数量的参数。

参数包在模板中表示为 ...,可以出现在模板参数列表、函数参数列表、函数参数类型列表、模板参数列表中,用来表示可变数量的参数。

使用方法(摘自cppreference

1. 在变参类模板中使用

可以用任意数量的模板实参实例化:

template<class... Types>
struct Tuple {};

Tuple<> t0; // Types 不包含实参
Tuple<int> t1; // Types 包含一个实参:int
Tuple<int, float> t2; // Types 包含两个实参:int 与 float
Tuple<0> error; // 错误:0 不是类型

变参函数模板可以用任意数量的函数实参调用(模板实参通过模板实参推导推导):

template<class... Types>
void f(Types... args);

f(); // OK:args 不包含实参
f(1); // OK:args 包含一个实参:int
f(2, 1.0); // OK:args 包含两个实参:int 与 double

类模板中,形参包必须是类模板形参列表中的最后一个。

template<typename U, typename... Ts>    // OK:能推导出 U
struct valid;
// template<typename... Ts, typename U> // 错误:Ts... 不在结尾
// struct Invalid;

函数模板则可以出现在前面,只要其后所有的形参都可以从函数实参推导或默认拥有实参即可

template<typename... Ts, typename U, typename=void>
void valid(U, Ts...); // OK:能推导出 U
// void valid(Ts..., U); // 不能使用:Ts... 在此位置是不推导语境

valid(1.0, 1, 2, 3); // OK:推导出 U 是 double,Ts 是 {int, int, int}
2.变参函数模板的函数形参列表中使用

可以在变参函数模板的函数形参列表中使用,表示多个类型的形参。

#include <iostream>

template <typename... Args>
void printAll(Args... args)
{
((std::cout << args << std::endl), ...); // c++17
}

int main()
{
printAll(1, 2, 3, "hello", 3.14);
return 0;
}

在这个示例中,printAll 函数接受任意数量的参数,使用折叠表达式展开参数包,并打印所有参数。

可以使用类型约束加包名的形式定义:

#include <iostream>

template <std::intergal... Args>
void printAll(Args... args)
{
((std::cout << args << std::endl), ...); // c++17
}

int main()
{
printAll(1, 2, 3, 4, 5); //可行,都是整型
printAll(1, 2, 3, "hello", 3.14); //不可行,因为形参包的参数类型被约束为整型
return 0;
}

OpenGL世界坐标与窗口坐标

世界坐标就是物体在所创建的投影中的实际坐标,这些物体被投影矩阵转化成屏幕空间标准化坐标(NDC坐标),这些两个坐标(即世界坐标与NDC坐标)是通过mvp矩阵进行转化的。

而窗口坐标是显示在窗口中的像素的坐标,它与NDC可以通过归一化来转换。

因此,在OpenGL中,当鼠标点击窗口中的每一个坐标时,可以通过一系列转换,将此坐标转换为世界坐标,以此来映射一些动作。

auto& app = Application::Get();

float x = (2.0f * Input::GetMouseX()) / app.GetWindow().GetWidth() - 1.0f;
float y = 1.0f - (2.0f * Input::GetMouseY()) / app.GetWindow().GetHeight();
// Mouse always in z plane, so z === 1.0
float z = 1.0f;

glm::vec4 screenPos = glm::vec4(x, y, z, 1.0f);
glm::vec4 worldPos = glm::inverse(m_Camera->GetViewProjectionMatrix()) * screenPos;

// normalize, devide w
worldPos /= worldPos.w;

// DION_WARN("x = {0}, y = {1}", Input::GetMouseX(), Input::GetMouseY());
// DION_WARN("Postion = {0} {1}", worldPos.x, worldPos.y);

int chessX = static_cast<int>(std::round(worldPos.x));
int chessY = static_cast<int>(std::round(worldPos.y));

if (chessX >= -7 && chessX <= 7 && chessY >= -7 && chessY <= 7)
{
m_ChessBoard.Drop(chessX, chessY, ChessBoard::ChessColor::White);
}

总体步骤为:

  1. 获取窗口坐标x,y
  2. 将窗口坐标转换为NDC坐标,此时坐标每个轴的值应在(-1,1)之间,z轴始终1.0f
  3. 获取投影矩阵和视角矩阵,如果有模型矩阵也要加上(即MVP矩阵)
  4. 获取转换矩阵的逆矩阵,与NDC坐标相乘,所得的坐标除以w轴以标准化

C++获取一个float值最近的整数

四舍五入:

float num = 3.6f;
int rounded = static_cast<int>(std::round(num));

向下舍入:

float num = 3.6f;
int floored = static_cast<int>(std::floor(num));

向上舍入:

float num = 3.6f;
int ceiled = static_cast<int>(std::ceil(num));

截取整数部分:

float num = 3.6f;
int truncated = static_cast<int>(num);
// 直接使用类型转换

GLFW窗口图标设置

int            width, height, channels;
unsigned char* image = stbi_load("./Gobang/assets/gobang.png", &width, &height, &channels, 4);
if (image)
{
GLFWimage images[1];
images[0].width = width;
images[0].height = height;
images[0].pixels = image;
glfwSetWindowIcon((GLFWwindow*)window, 1, images);
stbi_image_free(image);
}
else
{
std::cerr << "Failed to load icon image" << std::endl;
}

多线程通信

使用条件变量可以实现进程间的通信,以打断耗时循环。

std::mutex              mtx;
std::condition_variable cv;
bool g_stop = false;


void childThreadHandle(){
std::unique_lock<std::mutex> lock(mtx);
g_stop = false;
while(!g_stop){
auto timeout = std::chrono::seconds(10);
cv.wait_for(lock, timeout, [] { return g_stop; });
}
}

// In main or other thread
void stop(){
std::lock_guard<std::mutex> lock(mtx);
g_stop = true;
cv.notify_all();
}
显示评论