颜林林的个人网站

Linlin Yan's Personal Website

[不靠谱颜论] 为了“弱化”类型,C++有多拼?

2022-02-15 23:00

1

众所周知,C++ 是一门强类型语言。也就是说,每个变量在使用前,都需要非常明确地定义清楚类型。变量类型,指的是所占内存空间大小(几个字节)、表示方式(字符、整数、浮点数、有无符号等)、存储模式(自动变量、静态变量、外部变量等;我在之前《现代C++学习笔记(4):auto的前世今生》一文对此有少许介绍)。

清晰定义变量类型,有助于写出性能尽可能高的代码。然而,这其实挺反人性的,尤其在变量很多的中大型程序中,光是反复书写类型,并确保赋值与定义类型相匹配,就是很大的负担。

这种负担在过去曾经更加沉重。回想“上古时代”的匈牙利命名法,变量名的前缀能体现变量类型,很便于代码阅读时,望文生义地理解其类型。至今 WIndows API 的函数声明中仍然可见此类表达:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
HWND CreateWindowExA(
  [in]           DWORD     dwExStyle,
  [in, optional] LPCSTR    lpClassName,
  [in, optional] LPCSTR    lpWindowName,
  [in]           DWORD     dwStyle,
  [in]           int       X,
  [in]           int       Y,
  [in]           int       nWidth,
  [in]           int       nHeight,
  [in, optional] HWND      hWndParent,
  [in, optional] HMENU     hMenu,
  [in, optional] HINSTANCE hInstance,
  [in, optional] LPVOID    lpParam
);

然而其缺点在于,一旦需要调整变量类型,就得把通篇该变量名批量替换为新类型前缀。

2

相比之下,JavaScript 之类的弱类型语言,统一的函数定义,根据赋值内容自动调整变量类型,简直就是“傻瓜式优质待遇”:

1
2
3
4
// javascript
var i = 1;
var d = 2.5;
var s = "hello";

于是,C++11 起,我们有了auto

1
2
3
4
// C++11
auto i = 1;
auto d = 2.5;
auto s = "hello";

当然,C++ 没有因为“妥协”于这样的使用便利,而退化为“弱类型语言”。它仍然保留了强类型语言的本质,在上述代码编译过程中,会自动根据上下文,推断出正确的类型。强类型语言的优势仍然存在,减轻的是使用者的编程负担。

3

相对困难些的,是把结构体进行“弱类型化”。

日常编程过程中,我们经常需要把两个或多个变量组合起来使用,比如平面坐标(组合xy)、立体空间坐标(组合 xyz)、RGB 颜色(组合rgb三个分量)。传统的做法,我们都需要很正式地“吟诵”出:“struct blablabla ...”。

在 C++ 自带的标准模板库 STL 中,为此贴心地设计了一个神器:std::tuple。下面用一个例子简单进行展示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <tuple>
#include <string>

int main()
{
  std::tuple<int, double, std::string> a = { 1, 2.5, "hello" };
  std::cout << std::get<0>(a) << std::endl;
  std::cout << std::get<1>(a) << std::endl;
  std::cout << std::get<2>(a) << std::endl;
  return 0;
}

放在过去,实现上述代码,不得不单独定义一个结构体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

struct Tuple
{
  int i;
  double d;
  std::string s;
};

int main()
{
  Tuple a = { 1, 2.5, "hello" };
  std::cout << a.i << std::endl;
  std::cout << a.d << std::endl;
  std::cout << a.s << std::endl;
  return 0;
}

而到了 C++17 标准,对上述“临时组合类型”的定义,甚至可以简化到不需要写具体类型,而仅由后面的初始化表达式来决定:

1
2
// std::tuple<int, double, std::string> a = { 1, 2.5, "hello" };
std::tuple a = { 1, 2.5, "hello" };

代码大为简化,用起来更像“弱类型语言”了。

4

其实上述封装思想,在只有两个元素时,专门有一个std::pair进行支持,该类型也是我们最常用的映射类型(或者说字典类型)std::map的实现基础。std::tuple可以说是std::pair的泛化,允许自动形成任意多个元素的组合类型。

C++17 标准提供的“省略模板参数”的写法,也为std::map的使用,带来了超级便利:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>
#include <map>
#include <string>

using namespace std;

int main()
{
  map a = { pair{ "foo", 1 }, pair{ "bar", 2 } };
  cout << a["foo"] << endl;
  cout << a["bar"] << endl;
  return 0;
}

5

此外,在未来的 C++ 标准中,std::tuple很可能还会像其他容器类型一样,允许进行迭代访问(目前还不支持,但已经有类似的提案了):

1
2
3
4
5
tuple a = { 1, 2.5, "hello" };
for... (auto e : a)
{
  std::cout << e << std::endl;
}

而如果能够支持,这种“根据组合类型展开,可预见到循环的次数及对应类型”,将成为“编译期循环语法”,这可是原生的 struct结构体所不具备的“惊喜”。像是具备“反射(reflection)”功能的 Java 或 C# 语言,但其实一切“魔法”都在编译期发生,确保了运行时的程序效率,这将是其他语言难以望其项背的。

当然,在这个新标准提案被采纳之前,还是有一些借用可变参数个数模板机制来实现上述功能的尝试。具体可参见 C++ Stories 网站上的两篇新文章,很有趣的“魔法秀”:

--- END ---

注:本文发表于“不靠谱颜论”公众号,并同步至本站。