灰一只猫的编程日记 - 心得体会
type: channel
link: https://grauneko.com/index.php/category/idea/
description: 总要有些总结，总会有些心得。
items: C#.Net 跨平台调用 C++ 动态库的实现
items: 为指定类型定制不同实现——C++模板特化（上）
items: 一些SSH连接使用小技巧
items: 如何使用C++与虚函数表愉快地玩耍并掉进坑里

C#.Net 跨平台调用 C++ 动态库的实现
type: item
date: 2022-08-21T18:18:00+08:00
description: C#.Net是一门优秀的跨平台编程语言，有着C++级别的运行速度，和大量用以提升开发效率的语法糖，以及极佳的跨平台能力。近期使用C#接入了一个C++库，在此稍作一下总结。1. 动态库实现方案选择近期在使用C#实现一些功能时，需要接入C++库。但C++由于众所周知的原因，同名函数可能有多种重载，因此实现的函数声明根据参数和重载等的不同，会被编译器加上不同的符号，导致无法被正确调用。上述问题有两种解决方法：使用extern "C"声明来声明一个纯C类型的函数，这也是后来实际采用的方式；仍然使用C++方式声明函数，但是在C#调用时，需要根据实际生成的函数声明来调用。第二种方式其实属于一种hack，问题就在于，不同编译器实际生成的函数名并不一致，因此可能不同版本编译器、不同平台上，都需要分别解析一次实际符号，并修改C#里的调用源码。这种方式明显不实际且不通用。而第一种，使用extern "C"的方式声明，则是通用的跨语言交互方案。缺陷有两点：缺少了C++的重载；只能使用纯C类型（值，数组，结构，指针），不能使用C++类型。而它的好处也显而易见：不同编译器、不同平台下导出的符号表统一（即为函数名）；上述限制仅针对需要EXPORT的接口，内部仍然可以用C++实现，类、模板等不受影响；这两点缺陷在可以接受范围内，因此采用这种方式实现跨语言交互。既然使用了extern "C" ，这个问题的本质其实就变成了“如何使用C实现一个C++的动态库，并允许其它语言调用”。那这其实就已经是一个方案很成熟的领域了，分为这么几步：首先用C实现动态库，并导出预期的符号（函数与类型）；其次，在不同平台上分别编译，得到不同平台上的库类型。在调用语言内实现对应的接口、类型，并添加引用；在调用语言内使用接口，测试功能是否正常。2. 使用 C 实现 C++ 动态库extern "C"方案也会带来另一个问题：如果需要让C#调用C++库，那么必须通过纯C进行一次中转：C++的模板等必须使用纯C做展开，分别以不同的声明接入；C++的类型（无论是STL还是自定义class）以及类型上的方法也需要用纯C展开；比如我们有一个这样的类型：class Document { public: Docunemt(std::string title): title_(title), content_() { } void AppendLine(int32_t number) { content_.emplace_back(std::to_str(number)); } void AppendLine(const char* str, int len) { content_.emplace_back(std::string(str, len)); } void AppendLine(const std::string& line) { content_.emplace_back(line); } std::string GetContent() const { /* do some string join works */ } private: std::string title_; std::vector<string> content_; }C语言无法使用std::string，std::vector<std::string>，也处理不了三个不同的AppendLine 重载，更不用提（没有放进示例）的模板。因此，如果我们想要用导出为动态库，需要使用C语言以如下形式声明：// 首先定义DLLEXPORT宏，指定哪些函数是需要导出的 #ifdef WIN32 #define DLLEXPORT __declspec(dllexport) #else #define DLLEXPORT __attribute__((visibility("default")) #endif class Document; // 这里是用于C++的前向声明 using i32 = int32_t; extern "C" { struct CppString { char* str = nullptr; int32_t len = 0; } DLLEXPORT Document* CreateDocument(const char* title_buff, i32 len); DLLEXPORT void AppendLineInt(Document* doc, i32 number); DLLEXPORT void AppendLineStr(Document* doc, const char* str, i32 len); DLLEXPORT void GetContent(Document* doc, CppS…

为指定类型定制不同实现——C++模板特化（上）
type: item
date: 2021-04-12T22:38:00+08:00
description: 0. 何为模板特化C++模板最普遍的用法是为一个（编译期确定的）未知类型定义类或者函数。但有时会遇到这样一种情况：这些通用的实现并不满足某些特定类型下的需求。这时，就需要使用C++模板特化，为指定类型定义一个不同的实现。C++的模板特化有两种：全特化（可用于类或函数）以及偏特化（仅能用于类）。全特化意味着，该实现的所有模板参数都已经被确定，对应的，偏特化表示其中只有部分模板参数是确定的。本篇将只介绍全特化，后者使用场景不多，等下一篇再展开介绍。2. 全特化 in action2.1. 定义一个使用场景在工程中比较底层的代码中，经常会遇到需要做特殊处理的模板类型。例如，定义一个通用的to_str<T>()函数，或者定义一个lesser_than<T>(a, b)，不同类型有着不同的处理方式。本篇以实现通用的to_str<T>(v)函数为例，简单介绍全特化的使用方法。2.2. 定义一个通用的实现首先我们定义一个通用的函数如下：// to_str.h template<typename T> std::string to_str(const T& t) { std::ostringstream oss; oss << "trival value: [" << t << "]"; return oss.str(); }内容很简单，无非是把typename T类型使用std::ostringstream进行导出处理（关于<<操作符在后面会有解释，见第3节）。现在，我们可以在任何地方，只要引用了这个文件，就可以做一些简单的转字符串操作了： std::cout << "Trival Values:" << std::endl; std::cout << to_str(32) << std::endl; std::cout << to_str("const char *") << std::endl; std::cout << to_str(34.56) << std::endl;2.3 声明并定义一个全特化实现但假如我们有这样一个类，很明显是无法直接使用这个函数进行to_str()操作的：// my_special_class.h class MySpecialClass { public: int val1; int val2; explicit MySpecialClass(int val1, int val2) : val1(val1), val2(val2) { } };2.3.1. 普通实现首先我们声明一个定义如下：// to_str.h template<> std::string to_str<MySpecialClass>(const MySpecialClass& t);有三点可能会引起注意的地方：template<>标明这是一个模板相关的实现；这里只进行了声明，但并没有做任何定义；模板特化声明要放在通用类型的声明与定义之后，否则编译器不认识这个声明。这时，我们将在头文件以外实现它的定义，此处放到to_str.cpp中：// to_str.cpp #include "to_str.h" #include "my_special_class.h" template<> std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); }这样我们就可以如下使用to_str()函数了： MySpecialClass spclass{ 12, 34 }; std::cout << to_str(spclass) << std::endl;2.3.2. inline实现普通实现中，可以注意到，它像一个普通的函数一样，将声明与定义分拆到了.h头文件和.cpp源文件中。为什么不直接将如下定义像模板通用实现一样放到.h头文件中呢？// to_str.cpp template<> std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); }原因是，已经全特化的函数就等于一个普通的函数，在头文件中进行定义会遇到那个经典的“重定义”问题。解决方法也很简单，要么如前一小节中所述，拆分声明与定义，要么在函数声明中加入inline关键字，这也表示着“使用该定义做展开”：// to_str.h template<> inline std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); }3. 对<<操作符的一点解释在本示例中，还有一种方法，可以不使用全特化，但仍然可以用…

一些SSH连接使用小技巧
type: item
date: 2020-07-15T23:31:00+08:00
description: 0. 背景最近折腾腾讯云轻量香港服务器，然而公司内网由于一些特殊设置，无法直接SSH到境外服务器上，不得已通过自己一个位于北京的VPS节点进行中转，经过一些查阅整理，总结了几个小技巧。1. VSCode Remote通过中转节点连接目标服务器VSCode的远程开发套件在『使用Docker作为C++开发环境：适用于CLion与VSCode的配置』里略有介绍。配置起来很简单，但如果需要使用中转怎么办呢？（以Windows场景为例）例如我有一个1.1.1.1作为中转节点，有一个2.2.2.2是我的目标节点。那么需要点左下角打开远程开发工具，选择这里编辑.ssh_config：Host jump_box HostName 1.1.1.1 User user Host target HostName 2.2.2.2 User ProxyCommand C:\Windows\System32\OpenSSH\ssh.exe jump_box netcat -w 120 %h %p这里的原理是使用jump_box的netcat进行转发，只要提前在1.1.1.1和2.2.2.2上的~/.ssh/authorized_keys里添加好本机的SSH Public Key即可。2. Windows Terminal中配置SSH Keep-aliveWindows Terminal由微软开发，是Windows下非常好用、几乎超过所有其他命令行的一个终端工具。在Windows Terminal中，可以添加一个默认操作的链接选项，添加如下配置，效果如上图所示。 "list": [{ "guid": "{b453ae62-4e3d-5e58-b989-0a998ec44144}", "name": "graueneko.com", "tabTitle": "graueneko.com", "commandline": "ssh -o ServerAliveInterval=60 ubuntu@graueneko.com", "cursorShape": "filledBox", "icon": "ms-appx:///ProfileIcons/{574e775e-4f2a-5b96-ac1e-a2962a402336}.png" }, ... ]由于网络中间常常有些中转服务器在链接太久没有数据传输时强制中断掉，因此为避免这种情况，这里使用了`ssh -o ServerAliveInterval=60"来为SSH每60秒发送一次心跳包，保持SSH有数据传输避免被中断。3. Windows Terminal配色在Windows Terminal的settings.json中，可以添加如下配色选项（使用的是OneDark Theme），让命令行更好看些：{ "schemes": [ { "name": "OneHalfDark", "black": "#282c34", "red": "#e06c75", "green": "#98c379", "yellow": "#e5c07b", "blue": "#61afef", "purple": "#c678dd", "cyan": "#56b6c2", "white": "#dcdfe4", "brightBlack": "#282c34", "brightRed": "#e06c75", "brightGreen": "#98c379", "brightYellow": "#e5c07b", "brightBlue": "#61afef", "brightPurple": "#c678dd", "brightCyan": "#56b6c2", "brightWhite": "#dcdfe4", "background": "#282c34", "foreground": "#dcdfe4" } ], }添加的位置如下：4. SSH连接接后加载颜色文件我很喜欢腾讯云自带的~/.bashrc文件配色，效果如下，可执行文件为绿色，文件夹为蓝色，普通文件是白色，正常软链接为青色，出错的软链接为红色，等等：没有这个~/.bashrc配置的同学可以使用这里的文件（我直接把它抄过来了）# ~/.bashrc: executed by bash(1) for non-login shells. # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) # for examples # If not running interactively, don't do anything case $- in *i*) ;; *) return;…

如何使用C++与虚函数表愉快地玩耍并掉进坑里
type: item
date: 2020-04-19T01:08:00+08:00
description: 0. 背景知识虚函数表不必多说，是每位C++学习者都要面对的问题。简单来说，这是C++面向对象的基础，保证子类指针转换为父类指针类型后，调用成员函数时仍然是子类的成员函数。听上去有点绕，但如果这一部分理解不了，还请在继续阅读前，先去学习一下虚函数相关内容。虚函数表大家都知道，是存放每个对象所有虚函数的指针的一块内存区域，指针指向真正的虚函数代码。《深入理解C++对象模型》一书中讲到：同一个Class下的所有对象共享同一个虚函数表；子类会继承父类的虚函数表（并不是直接使用，而是复制一份），并在此基础上做拓展；带有虚函数表的对象大小最小（空对象）为32 or 64 bit (4 or 8 Byte)，也就是虚函数表指针的大小；与之相反的，无虚函数的空对象最小为1 Byte作为占位；调用虚函数时，相比直接调用成员函数多了一步，首先是通过对象头的虚函数表地址找到对应虚函数地址，然后才是调用对应的虚函数。有必要备注一下：第3条虽然书中说是编译器相关的，但经过测试发现，Clang/GCC/MSVC都是这么做的，因此可以视这为现行标准之一。说了这么多，虚函数表有什么可以玩的东西呢？先别急，我们再看一下override与final关键词。在成员函数声明中，override关键词表示覆写父类虚函数，且可以继续被后续子类覆写；而final关键词表示覆写父类虚函数，且禁止被后续子类覆写。这两个关键词存在时，均暗含了“此函数为virtual”的意思，因此额外声明一个“virtual”时IDE会提示冗余。那么我们开始和虚函数表玩耍吧，玩什么呢？交换对象的虚函数表。1. 示例代码[scode type="blue"]示例代码可以在64位机型上直接运行，且尽可能简化以突出主题，希望读者如果有不清楚的地方，最好还是自己亲自尝试一下。[/scode]#include <cstdio> #include <cstdint> // 引入uint64_t // 我们从定义三个类开始，分别为A，B，C，且B继承A，C继承B class A { public: virtual void Print() { printf("p - A, "); } virtual void PrintWithOverride() { printf("po - A, "); } virtual void PrintWithFinal() { printf("pf - A\r\n"); } }; class B : public A { public: virtual void Print() { printf("p - B, "); } virtual void PrintWithOverride() override { printf("po - B, "); } void PrintWithFinal() override { printf("pf - B\r\n"); } }; class C : public B { public: void Print() { printf("p - C, "); } void PrintWithOverride() override { printf("po - C, "); } void PrintWithFinal() final { printf("pf - C\r\n"); } }; int main() { A *a = new A(); B *b = new B(); C *c = new C(); // 获取虚函数表指针 uint64_t avpt = *(uint64_t *) a; uint64_t bvpt = *(uint64_t *) b; uint64_t cvpt = *(uint64_t *) c; // 打印每个类里，虚函数表和三个虚函数的指针 printf("VPT & VFuncs: \r\n"); printf("A: %p - %p, %p, %p\r\n", avpt, *(uint64_t *)avpt,*((uint64_t *)avpt+1),*((uint64_t *)avpt+2)); printf("B: %p - %p, %p, %p\r\n", bvpt, *(uint64_t *)bvpt,*((uint64_t *)bvpt+1),*((uint64_t *)bvpt+2)); printf("C: %p - %p, %p, %p\r\n", cvpt, *(uint64_t *)cvpt,*((uint64_t *)cvpt+1),*((uint64_t *)cvpt+2)); // 打印正常情况下的输出 printf("\r\nBefore swap:\r\n"); printf…