TimothyQiu's Blog - C++
type: channel
link: https://timothyqiu.com/tag/cpp/
items: 使用 Conan 管理 C++ 依赖
items: 我所知道的 constexpr
items: std::error_code 和它的朋友们
items: 《C++ 并发编程实战》读书笔记： 管理线程
items: 用 STL 寻找前 K 个最大的数
items: Windows 核心编程：字符串查漏补缺
items: 小试 Variadic Template
items: 字符串之惑
items: std::string comparison
items: Cocos2d-x 笔记：人类一大步

使用 Conan 管理 C++ 依赖
type: item
date: 2020-03-07T06:39:00+00:00
description: 现代 CMake 使用 C++ 依赖项时已经相对方便了。比如 find_package(OpenSSL 1.0 REQUIRED) 就可以自动在本地机器上查找已安装的与 OpenSSL 1.0 兼容的包，然后就可以直接 target_link_libraries(targetName PRIVATE OpenSSL::SSL) 使用，不必再手动写头文件和库文件配置。 要做到「本地机器上已安装」，macOS 和 Linux 系统都非常方便，大多数库都可以直接通过系统级包管理工具安装，而 Windows 上就稍显麻烦。我之前比较喜欢的是，比较复杂的库还是留着在各平台手动安装，简单的则使用 CMake 的 FetchContent 模块下载使用。但这样的缺点是每次重新生成工程时，都需要下载一遍依赖并编译。尤其最近国内的网络状态，HTTPS 连接 GitHub 经常超慢。所以想想还是用现成的包管理工具吧。 目前市面上流行的包管理工具中：vcpkg 虽然很多人用，但是微软那套逻辑我始终表示审美不能；Hunter 虽然是纯 CMake 解决方案，但是官方涵盖的库偶有不足；而 Conan 我一开始是不喜欢的，不但需要使用 Python 安装，而且它的前世 biicode 当年也风光过现在已经挂了。不过现在看来，Conan 似乎是这几个之中比较成熟好用的解决方案，至少，符合我的审美就是了。 安装 官方推荐用 Python 在虚拟环境里用 pip install conan 安装，可以随时保持最新。 我在 macOS 上使用 Homebrew 安装，可以少管理一个虚拟环境。感觉 Arch Linux 这样滚动更新的系统也可以直接使用系统包管理工具安装。 找到想要的库 想要使用 spdlog 时，首先搜索： $ conan search spdlog -r conan-center Existing package recipes: spdlog/0.14.0@bincrafters/stable spdlog/0.16.3@bincrafters/stable spdlog/0.17.0@bincrafters/stable spdlog/1.0.0@bincrafters/stable spdlog/1.1.0@bincrafters/stable spdlog/1.2.1@bincrafters/stable spdlog/1.3.0@bincrafters/stable spdlog/1.3.1@bincrafters/stable spdlog/1.4.1@bincrafters/stable spdlog/1.4.2 spdlog/1.4.2@bincrafters/stable spdlog/1.5.0 命令行中的 -r conan-center 表示所要搜索的仓库，conan-center 是官方自带的默认仓库，如果你本地添加了多个仓库的话，也可以用 all 表示搜索所有仓库。不带这个选项时则是在本地的缓存中查找。 搜索结果中每一行都是一个可用的包的名称，使用 @user/channel 后缀的是完整的包命名方式。官方 conan-center 仓库中，最近的包都是通过 CI 自动构建二进制文件的，这些包使用纯 name/version 的命名方式。 想要知道某个版本/包的详情，可以使用这样的命令查看： $ conan inspect spdlog/1.5.0 会列出一些信息和安装时的可选参数。 当然，你也可以直接在网站 https://conan.io/center/ 查找 conan-center 仓库中的包。 依赖的指定、安装、使用 一般使用名为 conanfile.txt 的纯文本文件指定依赖，格式类似 INI 文件。 [requires] spdlog/1.5.0 [generators] cmake_find_package [requires] 部分很简单，列出你所需要依赖的包的名称即可。[generators] 部分指定所需要的「生成器」，可以生成与 CMake、SCons 等工具的对接文件。 使用 conan install /path/to/source-dir 可以安装依赖并生成对接文件，参数为包含 conanfile.txt 的目录。当然，这样做会把「对接文件」生成在当前目录，可以使用 -if 参数指定输出目录，推荐放在 CMake 的构建目录。 这样，Conan 就会把 1.5.0 版本的 spdlog 安装到自己管理的目录（一般是 ~/.conan），然后在输出目录输出一个 Findspdlog.cmake 文件。 CMakeLists.txt 中，要让 find_package 使用 Findspdl…

我所知道的 constexpr
type: item
date: 2018-04-25T14:51:00+00:00
description: 在知乎上看到《constexpr 究竟有什么用？》的问题，正巧前段时间趁着了解 constexpr if 的时候刚做过相关资料的搜集，拼凑了一下搞了一个回答。篇幅所限，完整版放在这里，主要是追加一些例子并补完一些周边内容 😄 constexpr 关键词是 C++11 引入的；C++14 中放宽了对 constexpr 函数的语法要求；而 C++17 则复用了该关键字，引入了 constexpr if。 constexpr 主要为 C++03 引入了以下变动： 拓展了「常量表达式」的概念 提供了「强制要求」表达式在编译时求值（compile-time evaluation）的方法 提供了编译时的 if 条件判断 拓展常量表达式的概念 之所以要拓展常量表达式的概念，是因为 C++03 中的一些尴尬，这从原本的标准库中就能看出来。 比如我们都知道 INT_MAX 是 C 语言的遗产，C++ 则更希望大家使用 std::numeric_limits<int>::max() 来拿 int 型的上限。然而不幸的是，后者是个函数调用而不是整型常量，使用起来可能需要花更多心思在性能或者别的东西上，没有前者那么自由。 int foo[std::numeric_limits<char>::max()]; // error: 不是常量表达式 又比如标准文件流，它的构造函数可以带上这样的第二个参数： std::fstream foo("foo.txt", std::ios::in | std::ios::out); 这个参数是 openmode 类型的，是由实现具体定义的一种 Bitmask 类型。出于类型安全的考虑，通常用枚举值实现而不是整型。但是这样一来就有个问题，同样是写 std::ios::in | std::ios::out，如果用整型的话可以作为常量表达式使用，而为了类型安全考虑换用枚举实现（尤其是重载 operator|）后，就再也不可能是常量表达式了。 inline openmode operator|(openmode lhs, openmode rhs) { return openmode(int_type(lhs) | int_type(rhs)); } 明明是这样简单的函数，对它的调用却不是常量表达式，就更别提编译时求值了。这就让委员会陷入了必须在「类型安全」和「效率」里二选一的尴尬境地。 标准库里会遇到这样的问题，大家日常使用也会遇到。加之标准委员会很想借此机会把原本标准中对于「常量表达式」（尤其是整型常量表达式）复杂的定义重构简化，引入 constexpr 来标记一些简单的函数，让对它们的调用能够作为常量表达式存在就很合情合理了。 而 constexpr 成员函数也可以类推，比独立的 constexpr 函数多一些对于类本身的限制便是了。 至于 constexpr 函数「到底应该多简单」，其实并没有必要刻意去记那些限制。如果你标记了 constexpr 的函数不够简单，编译器会提醒你哪里有问题的。 强制要求表达式编译时求值 说到这里，「constexpr 函数」并不能和「编译时求值」划等号，它只表达了「这个函数具备这样的能力」。 如果需要强制要求表达式在编译时求值，那么我们需要在变量定义前添加 constexpr，这样，用来初始化这个变量的表达式就「必须」是常量表达式，否则会报错。 #include <fstream> // 该函数不是 constexpr 函数 int identity(std::ios::openmode mode) { return mode; } constexpr auto out = std::ios::out; // 此处为 constexpr int main() { constexpr int modeFail = std::ios::in | identity(out); // 出错 int modePass = std::ios::in | identity(out); // 正常 } constexpr 函数只有同时满足 所有参数都是常量表达式 返回的结果被用于常量表达式（比如用于初始化 constexpr 数据） 才会触发编译时求值。如果只有参数是常量表达式而结果不是，那么是否触发编译时求值取决于具体实现。 constexpr if C++17 的 constexpr if 严格意义上不是 constexpr 而是 if 的一部分。 constexpr if 的主要用途是简化模板代码（这也意味着除非你是「库作者」或者「模版狂魔」，很少会用到）。很多原本需要绕弯借助类型 Tag 或者 SFINAE 来实现，需要拆成 N 个函数的功能，可以借助 constexpr if 写到一个函数里…

std::error_code 和它的朋友们
type: item
date: 2017-08-26T08:34:00+00:00
description: 前几天看 API 文档时候遇到了 std::error_code 这个东西，当时以为是 errno 的 Alias，后续查阅文档才发现并没有那么简单。 system_error std::error_code 包含在 <system_error> 头文件中，相关的共有四个组件： std::system_error std::error_code std::error_condition std::error_category std::system_error 是一个继承自 std::runtime_error 的异常类，用以表示与 OS 交互时得到的以错误码形式返回的错误。因此除了提供标准的 what() 函数外，它还额外暴露了一个返回 std::error_code 的 code() 函数。同时，因为必须提供错误码，它既不提供空构造、也不提供接受字符串的构造函数。 从名字上就可以看出来（同时也是当时的提案 N2241 中指出的），整个 <system_error> 头文件都是为实现这个异常而生的。STL 有意将 std::error_code 作为 std::system_error 的附属品，而非单独的错误处理机制。 std::error_code STL 中早期类似的错误码实现需求来自（C++17 才正式进入标准的）文件系统 API，当时的实现认为错误分类应在 errno 和操作系统原生错误码上二选一。但由于 STL 中网络、各种奇形怪状的 Boost 库的陆续加入，大家急需一个可扩展的错误码表示方案，才演变成现在的 std::error_code。 std::error_code 顾名思义表示错误码，是由一个 int 型的 value 和一个 std::error_category * 型的 category 组成的值类型类。 之所以在 value 外还需要保存一个 category，是因为即使是同样的错误码，在不同的库或者场景下表示的意义可能不同。同样是 42，在一个库的错误码中可能表示「文件不存在」，在另一个库中可能就表示「DNS 解析失败」。如果你熟悉 Cocoa 的那一套，那么这就类似 NSError 的 domain 属性。 此外，虽然没有明说 value 在等于 0 时表示无错误，但整个系统就是建立在这样的假设上的，例如： operator bool 是根据 value 是否非 0 来的 clear() 函数会将 value 设为 0 构造 std::error_code std::error_code 的构造函数共有三种重载： () 构造一个类似 clear() 后的对象 (value, category) 用参数填充对应的字段 (enum_value) 调用 make_error_code(enum_value) 工厂函数 前两种没啥好说的，第三种却值得推敲。 template<class ErrorCodeEnum> error_code(ErrorCodeEnum e) noexcept; 此处的 ErrorCodeEnum 只是名字上说是枚举，但其实只要是用户定义类型就行（比如 enum class / enum / class），所以理论上可以从异常直接构造 std::error_code。 通常，如果构造函数只接受一个参数，那么我们推荐将它标记为 explicit 以免发生不必要的隐式转换。但这个函数不然，它要的就是让原始的 ErrorCodeEnum 可以隐式转换为 std::error_code，从而实现这两者的直接比较。 catch (std::system_error const& e) { if (e.code() == std::errc::invalid_argument) { // blah blah blah... } } 当然，为了避免从任意类型的值都能搞个 std::error_code 出来，此重载只有当 std::is_error_code_enum<ErrorCodeEnum>::value 是 true 时才会有效。所以你自定义错误码枚举时，需要在 std 命名空间中特化此模版。 #include <iostream> #include <system_error> enum class YourErrorCode { kSuccess = 0, // 别忘了 0 应该表示无错误 kNetworkError, kBadRequest, kServerError, }; // 特化模版，启用对应的重载 namespace std { template<> struct is_error_code_enum<YourErrorCode>: true_type {};…

《C++ 并发编程实战》读书笔记： 管理线程
type: item
date: 2015-10-25T15:32:00+00:00
description: 最近终于找到本有兴趣的书啦，就是这本《C++ 并发编程实战》。我会找一些自己觉得没想到、好玩、或者有自己想法的地方整理下。 顺带吐个槽：In Action 系列的封面人物真心诡异，还是 O'reilly 萌萌哒的奇怪可爱小动物看着顺心。 join() 的异常安全 std::thread 要求在析构函数被调用时，该对象必须没有与之相关联的线程，否系统分分钟自尽给你看（调用 std::terminate()）。 一般情况下，我们主要需要注意的地方是析构前对 join() 或者 detach() 的调用。如果要分离线程，线程启动后就可以立即调用 detach()，基本不会带来什么问题。而 join() 则没法立即调用，这样就会带来可能会被忽视的异常安全问题：线程启动后，如果某处抛出异常，可能把本应执行的 join() 跳过，导致程序自行退出。 void foo() { std::thread t(thread_func); do_something(); // 如果抛出异常，就可能把下面的 join() 跳过 t.join(); } 把 do_something(); 用 try / catch 包裹起来，并在 catch 里调 t.join() 最后把异常重新 throw 出去的方案固然可行，但对于 C++ 而言，更好的方案是 RAII： class thread_guard { public: explicit thread_guard(std::thread& t) : thread_(t) { } ~thread_guard() { if (thread_.joinable()) { // join() 前对象必须满足 joinable thread_.join(); } } thread_guard(thread_guard const&) = delete; thread_guard& operator=(thread_guard const&) = delete; private: std::thread& thread_; }; void foo() { std::thread t(thread_func); thread_guard g(t); do_something(); } 当然你也可以考虑把 std::thread 移动进去而不是传引用，书里管那种东西叫 scoped_thread，调用时不必为 std::thread 对象单独开一个名字。 线程传参二三事 给线程函数传参是通过 std::thread 的构造函数完成的，其原理与 std::bind 类似。所有参数先在当前线程被复制 / 移动到对象内部，然后在新线程中完成对线程函数的调用。 这两步的划分会导致会被一些人说「坑」（虽然实际上是他们自己没注意）。 比如从「被复制」到「实际调用线程函数」之间可能并不连续，那么被复制的参数届时是否还有效就可能成为问题，例如： void thread_func(std::string const& s); void foo(int bar) { char buffer[1024]; sprintf(buffer, "%d", bar); std::thread t(thread_func, buffer); t.detach(); } 前面说过构造函数仅负责原样复制 / 移动参数，所以 char * 到 std::string 的转换是在新线程中的第二步时做的，于是例子中 t 对象构造函数暂存的其实是指向栈上的 buffer 的指针。而由于 detach()，foo 返回后可能新线程还未正式启动。如果 thread_func 真正被调用时 foo 已经返回，那么这个指针参数就无效了。 一个解法是把代码改成 std::thread t(thread_func, std::string(buffer));这样构造函数就会把实参 std::string 移动进去（即便在 std::string 无法移动只能复制的平行宇宙，这样做也是安全的）。

用 STL 寻找前 K 个最大的数
type: item
date: 2014-05-03T06:16:00+00:00
description: 前几天找资料，顺手看到了这篇博文，讲了博主如何优化自己以前的算法的过程。于是立马想起了 GoingNative 2013 上的演讲 C++ Seasoning 以及前不久看到的这篇 From goto to std::transform。于是不禁想，「寻找前 K 大数」这样的任务，能不能直接用 C++ 标准库来完成？如果能的话，性能又如何呢？于是我重新登录了那个以前做了两道题就尘封多年的 OJ 帐号…… 凭着最直观最 naïve 想法，第一个版本我用了 std::sort 来代替手工排序。不出所料，这样的做法跑了 562 ms，要排到 700 名开外的样子。 #include <stdio.h> #include <algorithm> #include <functional> #include <vector> int main() { int numVillagers; int topCount; while (scanf("%d %d\n", &numVillagers, &topCount) == 2) { if (numVillagers == 0 && topCount == 0) { break; } std::vector<int> wealth(numVillagers); for (int i = 0; i < numVillagers; i++) { scanf("%d", &(wealth[i])); } std::sort(wealth.begin(), wealth.end(), std::greater<int>()); if (topCount > numVillagers) { topCount = numVillagers; } for (int i = 0; i < topCount - 1; i++) { printf("%d ", wealth[i]); } printf("%d\n", wealth[topCount - 1]); } } 于是优化：考虑到每次新建 std::vector 的开销，把 wealth 拎出来放到循环外，reserve() 题目中的最大值，每次在循环里重新 resize() 后，时间顿时缩短到了 296 ms，可以排到 180+ 的样子。 std::vector<int> wealth; wealth.reserve(100000); 而在前面说过的演讲中，我还听到了一个之前从未留意的函数 std::nth_element。其作用是确保调用后第 N 个元素是范围内第 N 大的元素。调用后，[begin, N) 内的任意元素都小于 (N, end) 内的任意元素。 std::nth_element(wealth.begin(), wealth.begin() + topCount, wealth.end(), std::greater<int>()); std::sort(wealth.begin(), wealth.begin() + topCount, std::greater<int>()); 尝试修改成以上的略显罗嗦的代码后，程序运行时间缩短到了 171 ms，可以排到 70+ 的样子。时间上和博文中给出的最小堆实现相同，但是内存占用要比它大很多。 既然上面是先用 std::nth_element 大致排序了一下，而后再用 std::sort 排序前半部分，那么，STL 里是否存在一次性只排 [begin, N] 范围内的数，而无视 (N, end) 内的顺序的函数呢？答案是存在，可以直接使用 std::partial_sort 解决。 #include <stdio.h> #include <algorithm> #include <functional> #include <vector> int main() { int numVillagers; int topCount; std::vector<int> wealth; wealth.reserve(100000); while (scanf("%d %d\n", &numVillagers, &topCount) == 2) { if (numVillagers == 0 && topCount == 0) { break; } wealth.resize(numVillagers); for (int i = 0; i < numVillagers; i++) { scanf("%d", &(wealth[i])); } if (topCount > numVillagers) { topCount = numVillagers; } std::partial_sort(wealth…

Windows 核心编程：字符串查漏补缺
type: item
date: 2013-11-15T13:32:00+00:00
description: 趁着双十一买了好几本想买的书，其中就有这本《Windows 核心编程》。首先需要吐槽的就是好好的《Windows via C/C++》这么高端大气上档次的名字怎么就被翻译成了这么个蛋疼样，而且中文版的封面也是扑面而来的一股浓郁的上个世纪气息，以至于在正文里看到 Vista 的字样都感觉各种穿越…… 本文是关于这本书的第一篇读书笔记。天知道会不会有第二篇。 虽说之前已经写过好几篇关于字符编码的文章，都快写吐了，不过读了这本书的开篇还是感觉相见恨晚。这里对于前面这几篇里关于字符串的说明进行一下查漏补缺。 Unicode 和 UTF-16 前文书说到，Unicode 定义的是码位（Code Point），即为每一个字符赋一个唯一编号，记作 U+XXXX。Unicode 目前实际占用了 016 到 10FFFF16 的码位，有些已经分配了字符，有些还没有。 Unicode 中最初推出的 U+0000 到 U+FFFF 这 216 个码位称为基本多文种平面（Basic Multilingual Plane，BMP）。早年只有这一个平面的时候，Unicode 与 UTF-16 是等价的，因为可以做到一一对应。然而时过境迁，区区 16 位已经满足不了 U+10000 到 U+10FFFF 的码位了。 与 UTF-8 编码规则的爽快不同，UTF-16 在沦为实实在在的「编码」时，还需要考虑到向后兼容性问题。如何在维持 U+0000 到 U+FFFF 一一对应的同时，把剩余的 U+10000 到 U+10FFFF 编进来？ 答案是：把 U+D800 到 U+DFFF 命名为 UTF-16 编码专用字符。即如果 UTF-16 数据流中出现了这一范围内的字符，意味着它本身不是一个字符，紧接着它的 16 位数据需要加上一定的偏移值才是真正的数据。 这一土豪做法让我目瞪口呆。至于 UTF-32，目前来说与它与 Unicode 码位一一对应起来绰绰有余。当然，如果哪天发现了外星文明，需要把他们的字符也编码进来，导致 Unicode 占用的码位暴增，说不定这一一对应的任务就只能交给未来的 UTF-64 了。这就是把问题留给子孙后代去解决的大智慧啊！ CHAR 和 LPSTR 前文书里吐槽过 Windows API 的丑陋不堪，也说过这也是历史遗留、不得已而为之。 没错的，CHAR 和 char 在语义上并不等价，前者专指 8 位字符，后者则只是字符而已，说不定在哪个古董机器上就是 7 位了。（尽管我不认为你会在那上面跑 Windows……） 而 LPSTR 尽管被定义为 CHAR *，但它实际上还借助了编译器扩展实现了「以 NUL 结尾」的语义： typedef __nullterminated CHAR *LPSTR; Unicode 和 ANSI 版本 说实话我依旧不喜欢官方的这两个名字，我更喜欢「宽字符版本」和「多字节字符版本」这两个更拗口但更准确的名字。 由于现代 Windows 内部都是以 UTF-16 存储（承袭自 UCS-2），实际提供的 API 本身也都是 Unicode 版本的，即以 W 结尾的版本。而相应的 A 结尾的 ANSI 版本则是在 W 版本基础上的封装。 于是可以想象，全局使用 ANSI 相比全局使用 Unicode 而言要占更多的内存，理论上也要更慢一些。 以上。

小试 Variadic Template
type: item
date: 2013-10-07T15:57:00+00:00
description: 本文源自于今天对 neuront 童鞋的这篇文章的末尾的那段代码的 C++ 实现的思考。（好多「的」……） 尽管 std::accumulate() 和 Python 的 reduce() 类似，但因为 C艹 的 std::map<std::string, std::map<std::string, int>> 和 std::map<std::string, int> 是不同的类型，所以似乎只能自己用可变参数模板写一个了。 简单起见，我们还是退一步，来解决一个更简单、更不通用、而且似乎和 reduce() 完全无关的问题吧：如何才能一次性取得任意层次的字典值？用更直白的代码表达，就是我们需要一个 GetMapValue() 函数，实现这样的功能： // 用于缓解眼花缭乱感的宏 #define MAP_LITERAL(...) { __VA_ARGS__ } // 简单映射 std::map<std::string, std::string> simple_dict = MAP_LITERAL({"Hello", "World"}); // 「我勒个去居然这么麻烦」映射 std::map<std::string, std::map<std::string, std::map<std::string, int>>> nested_dict = MAP_LITERAL( { "x", MAP_LITERAL( { "y", MAP_LITERAL( { "a", 10 } ) }, { "z", MAP_LITERAL( { "b", 20 } ) } ) }); auto value1 = GetMapValue(simple_dict, "Hello"); std::cout << value1 << std::endl; auto value2 = GetMapValue(nested_dict, "x", "y", "a"); std::cout << value2 << std::endl; 初始的版本 初步分析，GetMapValue() 需要接受一个 Map 以及至少一个 Key。如此，参数可变，首选 C++11 的可变参数模板。一次性将所有 Key 拿到手后，每次用第一个 Key 获取下一级 Map，而后用余下的 Key 递归，最终获取所要的值。而要写递归，最好先从最简单的情况写起。 template <typename MapType> auto GetMapValue(MapType const& map, typename MapType::key_type const& key) -> typename MapType::mapped_type { return map.at(key); } 鉴于本人目前对于右值引用还不熟悉，就只用 const& 了（& 只能引用左值；const& 既能引用左值、又能引用右值，但无法修改）。 这里需要注意的是 STL 的几种 Map 里 typedef 到的数据类型，略坑： key_type 是 Key 的类型 mapped_type 是 Value 的类型 value_type 是 Map 中实际存储的键值对的具体类型（如 std::pair<key_type, mapped_type>） 不过这样一来，我们的 GetMapValue() 就可以在 STL 的这几种 Map 里通用了。 不被支持的递归 decltype 模板 本来我想，递归版本按照上面这个最简版本写一下就 OK 了。可变参数模板的基本用法我以前的日志有写过；这里唯一麻烦的是返回值，因为通用性，只有最终被调用的版本才知道确切的返回值，不过 decltype 似乎可以救场： template <typename MapType, typename... MoreKeyTypes> auto GetMapValue(MapType const& map, typename MapType::key_type const& key, typename MapType::mapped_type::key_type const& anotherKey, MoreKeyTypes... moreKeys) -> decltype(GetMapValue(map.at(key), anotherKey, moreKeys...)) { return GetMapValue(map.at(key), anotherKey, moreKeys...); } 然而，编译器不给面子，直接提示模板推导失败，decltype 时找不到接受两个 Key 版本的 GetMapValue()（三个参数）。 似乎，decltype 是不支持递…

字符串之惑
type: item
date: 2013-09-13T13:56:00+00:00
description: C 语言中的字符串 抽象数据类型 C 语言贴近机器模型，所以并不提供字符串数据类型。但是，字符串却在 C 语言中作为 ADT 存在着：使用字符指针代表字符串，同时提供了 string.h 作为字符串的操作库，而在实际存储时则使用字符数组。 虽说单引号引起的字符是该字符对应的一个整型常量，但是 'A' 和 65 没有任何关系。因为 C 语言并没有要求每个实现都用 ASCII 字符集作为执行字符集。 事实上，C 语言标准要求每个 C 语言实现都要定义两个字符集：代码字符集、执行字符集。每个字符集又都由基础字符和扩展字符构成。扩展字符可有可无，完全由该实现所支持的区域设置决定；基础字符只划定了字符，字符对应的码位留由实现决定。 字符与 char 类型 字符集顾名思义就是字符的集合，字符集除了划定所包含的字符外，还将其所包含的字符按照一定顺序编号，这样的编号称作码位。著名的 ASCII 参照已有电报报文设计，分别用数字 0 到 127 作为英文大小写字母、阿拉伯数字、标点符号等的码位。由于出现早、使用广，后辈字符集、字符编码几乎都与之兼容为荣。 然而 ASCII 本身并不完美，尽管名字叫「美国信息交换标准代码」，但是却连英语中的一些诸如 naïve、café、élite 的外来词都无法表示，欧洲各国那些稀奇古怪的字母就更别提了。所幸，我们很快来到了一个字节 8 位的年代，char 能够容纳 0 到 255 一共 256 个值，于是各国纷纷物尽其用，将一个字节中 ASCII 并没有用到的 128 到 255 对应为自己国家所需要用到字符。这些五花八门的对应规则便是后来的代码页（code page），同样的值可以在不同的代码页中表示不同的字符。 到目前为止，char 类型还算是名副其实的字符类型。然而，东亚文化圈广泛使用的汉字成千上万，少说也需要上千个码位，一个字节所能提供的这区区 256 个码位实在是杯水车薪。 计算机科学里的任何问题都可以通过增加一个中间层来解决。 既然码位不得不超过 255，一个字节放不下，那么将以前的「码位→字节」变成「码位→编码→字节序列」如何？ 举个栗子：Unicode 字符集中，字符「汉」的码位是 27721（十六进制 6C49），一个字节显然容不下，我们可以通过 UTF-8 编码将其变成字节序列。 通过字符集找到对应码位：汉→ U+6C49； 根据 UTF-8 编码规则，介于 U+0800 和 U+FFFF 之间的码位需要按照 1110xxxx 10xxxxxx 10xxxxxx 的形式编码为三个字节； 6C49 的二进制形式是 0110 1100 0100 1001，代入上面的模板得到 11100110 10110001 10001001； 得到最终的字节序列 E6 B1 89。 看上去不错，但这样一来，char 这个「字符」类型就有点名不副实了：一个 char 可能只是一个字符的一部分，只有两个或者更多的 char 拼起来才能够表示一个字符。更为麻烦的是，相关的标准库也变得不再可靠了：类似 strlen() 这种 too simple, sometimes naive 的方法顿时从返回字符串的字符数降级成了返回字符串的字节数。 printf("%zu\n", strlen("\xE6\xB1\x89")); // 输出 3；但实际上只有一个字：汉 多字节字符串 为了解决 strlen() 的这种尴尬，C 语言中引入了「多字节字符串」（multibyte string）的概念，一个字符可以对应多个字节；而原先的字符串则对应称为「字节字符串」（byte string），一个字符只能对应一个字节。 char const *multibyteString = "Hello 世界"; // 和普通字符串没什么区别 多字节字符串依旧是 char *，所以这两者的区别其实只是抽象概念上的，何况严格来说，字节字符串也是一种特殊的多字节字符串。 不同的编码每个字符占用的字节数可能不同，即便是同一种编码，不同的字符也可能占用不同的字节数。所以多字节字符串面临的主要问题便是「下一个字符到底占几个字节」。C 语言引入了 mblen() 函数来回答这个问题： int mblen(char const *s, size_t n); 其中 s 是一个多字节字符串，而 n 表示至多检查多少个字节；这个函数会根据当前区域设置（Locale）的 LC_CTYPE 分类中所指定的编码来解析这个多字节字符串，返回该字符串的字一个字符占几个字节。 char const *multibyte= "\xE6\xB1\x89"; // 「汉」的 UTF-8 编码 printf("%d\n", mblen(multibyte , 5)); // 输出 …

std::string comparison
type: item
date: 2013-08-09T14:38:00+00:00
description: 众所周知，char 类型是独立于 unsigned char 和 signed char 之外的一种实现相关类型，这着实带来了不少麻烦。 std::string a = "Hello"; std::string b = "世界"; // UTF-8 或者 GBK 皆可 那么请问，a[0] < b[0] 是什么结果？OK，那么 a < b 呢？ 当然，貌似在开篇头一句就已经被我剧透的情况下作出错误的回答还是蛮困难的。没错，无论是 GBK 还是 UTF-8 存储，答案都应该是： a[0] < b[0] 与实现相关。在我的 Windows 7 32-bit 和 Arch Linux 64-bit 上，都是 false a < b 永远为 true 前者很好理解，因为实质是两个 char 在比较，所以谁大谁小还得看 char 到底是无符号的还是有符号的。而后者为什么肯定是 true 呢？ 尽管 std::string 是 std::basic_string 在字符类型为 char 时的特化，但 C++ 标准中明确规定： The two-argument members eq and lt shall be defined identically to the built-in operators == and < for type unsigned char. 也算是 C++ 造福人类的某种方式吧。 以上。

Cocos2d-x 笔记：人类一大步
type: item
date: 2013-07-25T13:15:00+00:00
description: 上一次看 cocos2d-x 是去年的事情了，下了个 SDK 而后按照官方的那个 pew-pew-pew 教程接二连三捣鼓了几下，之后几乎就没怎么去碰了。最近再看感觉连当时自己写的代码都快不认识了，果然还是需要记下笔记 = = 获取 SDK 前往 cocos2d-x 官网下载稳定版打包，或者去官方 GitHub 仓库 git clone 一份（注意看清分支哟）。 个人这回是下载了最新稳定版本，目前是 2.1.4。 创建工程 Cocos2d-x 由若干部分组成，有些采用静态链接、有些则是动态链接、更有些是直接源代码分发的；加上工程本身的跨平台属性，需要针对不同平台写初始化代码；总而言之，从零开始手写一个空工程是相当繁琐的。 所以老版本的策略是：为不同平台下的开发工具提供模板、脚本之类的东西以便获得每次的初始空工程。比如它会为 Visual Studio 安装一个 cocos2d-x 程序模板，这样就可以直接在新建对话框里选择新建一个 Visual C++ 的空 cocos2d-x 工程了。 当然，从 2.1.4 版本开始，官方就不再为 VS 提供模板了，逐步在各平台采用统一的 Python 脚本创建跨平台工程。（吐槽：虽然不提供模板了，但是原先用来安装模板的 install-templates-msvc.bat 文件居然还在，虽然一运行就报错的说……） 这个创建脚本使用 Python 2.X，同时它还有一个 Bug：使用当前目录而不是脚本文件所在目录查找模板文件（GitHub 上的 develop 版已经修复）。所以，要创建工程，我们需要先从命令行进入 tools/project-creator 目录，然后再使用 create_project.py -project <工程名称> -package <Java 包名> -language <语言> 就会在 projects 目录下创建相应的工程。这一点在直接运行 create_project.py 时会给出提示。（3.0 版本将在 cocos2d-x 根目录提供 create-multi-platform-projects.py 文件为你间接调用 tools/project-creator/create_project.py）那么作为一个示例，我们可以输入： create_project.py -project Whatever -package com.timothyqiu.Whatever -language cpp 咳咳，如果你对为什么语言是 C++ 也要输入「Java 包名」感到困惑，那么看一眼生成后的工程目录结构就明白了： Whatever -+- Classes |- Resources |- proj.android |- proj.blackberry |- proj.ios |- proj.linux |- proj.mac |- proj.marmalade `- proj.win32 结构非常精巧：平台无关的（自己写的逻辑之类的）代码都在 Classes 目录下；程序用到的资源都在 Resources 目录下；其余的平台相关（预置的初始化之类的）代码及工程文件都在各自的目录下。 所以，既然有 Android 工程在，那么创建时显然是需要包名的。 渲染树 如果你熟悉 3D 也许你也熟悉「渲染树」的概念：加入渲染树的节点才能够被渲染，而节点的父子关系也对其位移、缩放、旋转有相应影响。渲染一帧图像的过程就是遍历整棵渲染树、依次绘制各个节点的过程。 cocos2d-x 中的节点，以 CCNode 表示，它除了作为一棵渲染树的节点外，还提供了定时回调以及执行 CCAction 动作的功能。当然， CCNode 还封装了一些类似位置、缩放、旋转的基本属性，其中锚点（Anchor Point）是非常重要的属性：节点的位置和节点左上角、右下角的位置是什么关系？缩放以哪个点为定点？旋转以哪个点为中心？这些问题的答案就是锚点。 cocos2d-x 中对象的属性使用 Getter/Setter 封装，均为 setPropertyName(value) 和 getPropertyName()、isPropertyName() 的形式。一些 CCNode 常用属性如下： 位置（Position）默认为 (0, 0)，是一个 CCPoint 类型的值。 旋转（Rotation）默认为 0，顺时针以角度计。 缩放（Scale）默认为 1.0f。 尺寸（ContentSize）默认为 (0, 0)，是一个 CCSize 类型的值。 可见（Visible）默认为 true。不可见和不存在是两码事。 锚点（AnchorPoint）默认为 (0, 0)。每个分量都是一个浮点数，0.0f 表示左上…