在Windows上的Visual Studio Code中配置C/C++运行环境

虽然在Windows上写C++程序首推Visual Studio,但是如果想要方便地运行代码片段或者编译单个文件的话,Visual Studio Code仍然是一个好选择。

首先去扩展商店中搜索并安装“C/C++”以及“Code Runner”。安装好了之后点击重新加载,以使插件生效。

Code Runner只是一个快捷运行代码的工具,我们需要准备一个编译器。如果已经安装有Visual Studio的C/C++桌面开发相关组件的话,MSVC应该是可以直接用的(只想安装VS 2017单独的C/C++工具链的话,也需要从官网下载VS的安装程序,然后只勾选必要项即可)。mingw或者WSL中的g++、Clang之类的也可以,看个人喜好。接下来以MSVC为例,配置Code Runner的命令行参数。

点击文件-首选项-设置,找到扩展标签下的Run Code configuration,修改Executor Map项(需要手动编辑JSON文件)。

fig1

settings.json分为左右两个窗格。左边是默认设置,右边是用户/工作区设置。我们在右边新增同名的键,以覆盖左边的默认值。

以命令行方式使用MSVC(cl.exe)需要配置环境变量。最简单的方式是运行VS的开发人员命令提示符脚本,它的位置可以在开始菜单里找到。

可以直接按照上面的样子配置。VsDevCmd.bat会设置相关环境变量等,然后直接cd到当前文件所在目录,调用cl即可。

配置好了之后,保存settings.json,打开一个C/C++源文件,按下F1打开命令窗口,输入Run,找到Run Code命令,运行之:

fig2

编译运行后应该能看到如下界面:

fig3

看起来一切顺利,但是还有问题:MSVC输出的中文会变成乱码。随便拿掉一个语句分号再编译,就会发现这一点。原因在于Code Runner使用管道重定向了命令输出,然后将其作为UTF-8文本显示。这在Linux下似乎没有大问题,然而由于历史原因,多数Windows下的命令行工具使用的是本机编码(GBK)。如果Locale配置无误的话,常用字符的显示在Windows终端下倒是没问题。但是我们这种情况就比较麻烦了,因为如果直接运行cl这类程序的话,它们的MBCS字符输出会被系统的ANSI系API正确处理,然后打到屏幕上;但是这里Code Runner面临的是未经处理的字节流,而它……看什么都觉得是UTF-8。虽然我们也可以通过调整code-runner.runInTerminal设置,让命令直接在终端里运行,但是比较彻底的方式是让cl直接输出UTF-8文本,或者……帮它输出。

写一个Wrapper对于熟悉Win32编程的读者来说应该不难:

稍微需要注意的是,由于GBK编码是变长的,所以当收到的第一个字节的最高位为1时,要等待第二个字节到来,再进行转换;由于我们的程序是要被Code Runner重定向STDOUT到匿名管道的,所以我们自己写控制台时就不能使用WriteConsole了,而应该使用通用的WriteFile。

编译好了之后,将这个程序命名为GBKWrapper.exe,然后丢到随便一个%PATH%内的目录即可。

要利用这个程序,只需要修改一下编译命令行:

在cl前面加上GBKWrapper,就可以看到中文的编译器输出了:

fig4

细心的读者可能看到了cl编译器选项的/utf-8。由于VSCode默认的UTF-8是不带BOM的,因此会被cl识别为当前本机编码(GBK)的文件。设置/source-charset:utf-8即可解决这个问题。/utf-8会同时设定source charset和execution charset,所以普通的字符(串)字面值也会变成UTF-8(当然,可以用前缀控制字面值编码,比如u8、u和U)。

C++中的traits和SFINAE,了解一下?

同样的内容已发布到Project 1论坛。https://rpg.blue/thread-406374-1-1.html

大扎好,我系喵^3,今天为大家介绍一下【今天】的C++造轮子时与编译器玩耍用到的两个小技巧。)咳
C++是静态类型的语言,所有对象的类型要在编译期决定。这给造轮子带来了一些麻烦,比如,在编写泛型方法的时候有时需要判断目标类型是否支持对应的操作
但是,C++这语言有些问题:缺少Metaclass,语言自省能力不足,再加上编译期必须推导出所有变量的类型,导致这种“判断”不好实现。(C++的所有高级抽象原则都是在尽量不造成太大开销的情况下实现的,这也是为什么C++这么快的原因之一)
不过由于C++的模板推导能力很强大,还是可以使用一些很hack的方式实现这类要求的。
标准库提供了大量的helper设施来判断某个类型的“特性”,比如是否可平凡析构、是否是数值类型,等等。
这里我没有用到它们,而是尝试自己推导了一组,虽然实现未必漂亮但过编译还是没问题的……233

首先我们定义两个类型,Foo和Bar。

CxxTraitsSFINAE1

Foo有两个double类型的成员taroxd和kuerlulu,而Bar只有一个taroxd,还是int类型的。

CxxTraitsSFINAE2

然后写一个函数fun,并分别用Foo和Bar类型对象作为参数调用它,经过一些蜜汁操作之后,我们可以区分fun中的抽象类型Ty是不是Foo类型,以及它有没有kuerlulu这个值(通过给Foo打了一个has_kuerlulu实现的,其实可以修改一下,直接去判断它有没有这个成员)

运行输出如下

CxxTraitsSFINAE7


看懂了嘛?懂了可以Ctrl+W了,不懂的话不着急,冷静分析.webp
为了实现fun的效果,我们需要在编译的时候计算出Ty的具体类型、以及它有没有kuerlulu这个成员,并调用正确的重载函数。如果不这么做,对于Bar这个类型,是不存在Bar::kuerlulu的,因此fun过不了编译。
因为一切的判断都是静态的,是充分利用编译器类型推导的结果,所以思维方式要转变一下,这里的“C++”其实并不是跑起来的C++,更像是一个函数式语言…
(顺便一提,利用模板类型推导可以进行计算,并且被证明是图灵完全的,感兴趣的可以写个编译期阶乘之类的~小心不要把编译器给爆栈了哦)

首先做两个辅助类型

CxxTraitsSFINAE3

其实就是两个空的结构体,里面的using是在结构体内部的可见域里定义类型的别名(例如using uint = unsigned int;),并不是成员啦。
还有一个括号运算符重载,用来获得对应的布尔值true/false
注意到static_false里没有is_true,static_true里没有is_false,具体有什么用后面再说。

CxxTraitsSFINAE4

然后做一个is_foo类型来判断。这里利用了C++的模板特化
假设有一个模板结构类型is_foo<T>,然后我们特化了一个版本叫is_foo<Foo>,那么编译器在模板实例化时会选择“更完美匹配”的版本,这里的效果就是其他类型会匹配到第一个,Foo类型会匹配到第二个。
自然,如果用int实例化,is_foo<int>里面的value就定义为static_false类型了,所以在编译期【is_foo<int>::value】这个类型就是static_false
大概就是这种思路,我们用类型定义来代替运行期的“变量”的概念

现在已经搞定了fun前半部分的is_foo操作了。由于is_foo<int>::value是一个类型,is_foo<int>::value()就是默认构造了一个is_foo<int>::value类型的对象的意思,is_foo<int>::value()()就是调用了这个对象的operator(),返回一个bool值。根据这个进行if选择就好了。
我们在写泛型轮子的时候,就可以利用这类手段去针对特定类型执行对应操作。

那教练…有没有更给力的?

我们永远不会知道我们的用户会塞进来什么样的类型呀,指明类型的名字显然不是一个好主意。更好的做法是判断一个类型是否具有某种“特性”,这就是traits,可以理解为特性萃取
事实上标准库已经这么做了。比如算法库会根据你传进来的迭代器类型,查询iterator_traits里的信息,比如你这迭代器是否支持随机访问,迭代器解引用之后的类型是什么…然后选择效率最高的算法实现。没错,都是编译期决定的。

所以我们也来写一个吧。

CxxTraitsSFINAE5

foo_traits_base类型是用来保存某个信息对应的两个traits默认值的。value_type定义为Ty类型里的tag_value_type的别名,而has_kuerlulu定义为static_false类型的别名。
普通版的foo_traits<Ty>直接继承了base,而Foo这个类型可以定义自己的foo_traits<Foo>特化(通常这个定义和你的Foo类是一块提供的,我是为了更清楚的叙述逻辑才放到这里的)
还是和is_foo类似的思路,foo_traits<Bar>::has_kuerlulu就是static_false类型的别名。只有Foo这个类型的has_kuerlulu是static_true的别名。这就相当于你提供一个类型Ty,然后foo_traits这个类型就可以把Ty的一些“特性”提取出来。
而value_type我们希望它表示Ty里面的成员的类型(Foo是double而Bar是int),它是从这两个结构定义里的tag_value_type获得的。可以翻上去看看

接下来就是上面这堆东西的真正用法了。

CxxTraitsSFINAE6

这两个版本的get_kuerlulu_if是在fun里面调用的函数重载候选。我们需要根据给fun的Ty类型里有没有kuerlulu来决定调用哪个版本。所谓的SFINAE就是“替换失败不是错误”,意思是编译器用实际类型X在匹配重载的模板候选P时,如果将模板参数Ty替换为X时会导致病式(ill-formed),那么不会产生编译错误,而是将候选P从重载候选中移除。我们就利用这个规则,让编译器在没有kuerlulu时拒绝候选2,从而接受候选1,什么都不做;当Ty拥有kuerlulu时,拒绝候选1而接受候选2。

利用SFINAE有很多种形式,比如在函数参数里加一个类型,让它在特定条件下为病式,但是这样就多了一个参数,看起来很不好(编译器会不会把它优化掉另说…)
所以这里我们选择从返回值做文章。这两个版本的返回值都是void。
Sb在实例化的时候我们放上了foo_traits<Ty>::has_kuerlulu。再强调一遍,它是个类型别名,可能是static_false<>或者static_true<> (我把它们写成了模板,默认参数其实有一个Ty=void)
为了帮助理解,我们分类讨论。
假设Sb的类型是static_false<void>:
那么根据定义,对第一个版本,static_false<void>::is_false就是void的别名,函数返回值就被替换为void,完美。
对第二个版本,由于static_false<void>中不存在is_true这个定义,因此为病式,这个版本被拒绝。
假设Sb的类型是static_true<void>,类似地,第一个版本被拒绝,第二个版本匹配上了。

到此为止,就有了fun中的那套神奇操作。


我的看法:
有效吗?有效。
为什么写法这么丑,就像做数学证明题?因为这套操作并不是语言有意设计的,纯属这个语言的类型推导过于牛逼。
有更好的写法吗?比如给C++增加更多的语言特性,编译期反射啊元类啊Concepts啊…这个就要等待语言的发展了,目前C++是一个过于复杂的语言,拥有巨量的语言特性(和坑),导致这个语言极难掌握、极难精通。←“精通C++”已经成为一个梗了,类似“PHP是最好的语言”一样的存在。作为萌新的我自己连“熟练”都不敢说呢…听说有人想21天?)雾
所以再增加新特性的话必须慎之又慎(不考虑兼容性的话甚至还要砍吧),期待那群语言律师在C++20(如果不鸽的话)能拿出更棒的设计来,让这个语言写起来更靠谱、更友好。

 

Win32控制台数独

鼠标或键盘方向键移动光标
输入1~9填入数字,Backspace清除
从同文件夹data.txt读取题目,每行读取9个字符,空格用0或者其他字符代替

 

Win32控制台扫雷

之前用C++实现了一个使用鼠标操作的Win32 Console扫雷游戏。

源代码如下,使用Visual Studio 2017编译通过。

 

C++实现快速排序算法