Skip to main content

meta-programming

·2019 words·10 mins
WFUing
Author
WFUing
A graduate who loves coding.
Table of Contents

元编程是编写可在其他程序上运行的计算机程序的技术。诸如编译器和程序分析器之类的系统可以被视为元程序,因为它们将其他程序作为输入。我们将在这里讨论的元编程形式特别关注生成要作为程序一部分包含的代码。从某种意义上说,它们可以被认为是初级编译器。

Macros and Code Generation
#

macro 是将输入序列转换为某种替换输出序列的规则。这个翻译过程称为 macro expansion,一些语言提供宏作为其规范的一部分。宏设施可以被实现为 preprocessing step,其中宏扩展发生在 lexical and syntactic analysis 之前,或者它可以被合并为 syntax analysis 或 a later translation step。

使用最广泛的 macro systems 之一是 C 预处理器(CPP),它作为处理程序的第一步被包含在 C 和 C++ 中。预处理器指令以散列符号开头,包括 #include#define#if 等。例如,下面定义了一个类似函数的 macro 来交换两个项目:

#define SWAP(a, b) { auto tmp = b; b = a; a = tmp; }

然后,我们可以如下使用宏:

int main() {
  int x = 3;
  int y = 4;
  SWAP(x, y);
  cout << x << " " << y << endl;
}

// output
4 3

通过向 g++ 传递 -E 标志,可以获得宏扩展的结果:

$ g++ -E <source>

不过,如果使用 #includes 指令,结果可能会非常混乱,因为该指令会从给定文件中调入代码。

CPP macros perform text 替换,因此上述代码等同于:

int main() {
  int x = 3;
  int y = 4;
  { auto tmp = y; y = x; x = tmp; };
  cout << x << " " << y << endl;
}

使用 SWAP 宏后的分号仍然保留,表示空语句。不过,在需要使用单一语句的情况下,例如一个没有被 block 括住的条件分支,这就会造成问题:

if (x < y)
  SWAP(x, y);
else
  cout << "no swap" << endl;

避免这一问题的常用方法是将 macro 的扩展代码放在 do/while 中:

#define SWAP(a, b) do {                         \
    auto tmp = b;                               \
    b = a;                                      \
    a = tmp;                                    \
  } while (false)

在这里,我们在一行的末尾添加了一个 \,表示下一行应被视为上一行的继续。do/while 循环在语法上以分号结束,因此 SWAP(x, y); 中的分号在语法上是 do/while 循环的一部分。因此,扩展代码的语法是正确的:

if (x < y)
  do { auto tmp = b; b = a; a = tmp; } while (false);
else
  cout << "no swap" << endl;

虽然 textual replacement 很有用,但它也有缺点,因为虽然宏在语法上类似函数,但它们的行为却不像函数。具体来说,它们不把参数作为自己的实体,也不引入独立的作用域。请看下面的例子:

int main() {
  int x = 3;
  int y = 4;
  int z = 5;
  SWAP(x < y ? x : y, z);
  cout << x << " " << y << " " << z << endl;
}

// output
3 4 3

使用 g++ -E,我们可以看到预处理后的代码。只看 main() 的输出,我们会发现

int main() {
  int x = 3;
  int y = 4;
  int z = 5;
  do {
    auto tmp = z;
    z = x < y ? x : y;
    x < y ? x : y = tmp;
  } while (false);
  cout << x << " " << y << " " << z << endl;
}

在这里,我们手动添加了换行符和空格,以使输出更易读;而预处理器本身会将宏输出放在一行中。罪魁祸首是最后生成的语句:

x < y ? x : y = tmp;

在 C++ 中,条件运算符 ? : 和赋值运算符 = 具有相同的优先级,并且从右向左关联,因此这等同于

x < y ? x : (y = tmp);

由于 x < y,这里没有赋值。因此,x 的值保持不变。

我们可以在每次使用 macro argument 时加上括号来解决这个问题:

#define SWAP(a, b) do {                         \
    auto tmp = (b);                             \
    (b) = (a);                                  \
    (a) = tmp;                                  \
  } while (false)

现在会产生预期的结果,因为括号明确地将这些操作联系起来:

int main() {
  int x = 3;
  int y = 4;
  int z = 5;
  do {
    auto tmp = (z);
    (z) = (x < y ? x : y);
    (x < y ? x : y) = tmp;
  } while (false);
  cout << x << " " << y << " " << z << endl;
}

然而,第二个问题并不那么容易解决。考虑一下当我们将 SWAP 宏应用于名为 tmp 的变量时会发生什么:

int main() {
  int x = 3;
  int tmp = 4;
  SWAP(tmp, x);
  cout << x << " " << tmp << endl;
}

// output
3 4

没有发生交换!再次使用 g++ -E 检查输出,我们可以看到(模数间隔)

int main() {
  int x = 3;
  int tmp = 4;
  do {
    auto tmp = (x);
    (x) = (tmp);
    (tmp) = tmp;
  } while (false);
  cout << x << " " << tmp << endl;
}

由于 SWAP 使用的临时变量与参数同名,因此临时变量会捕获生成代码中出现的参数。这是因为宏只是执行文本替换,并不能确保名称被解析到适当的作用域。因此,宏实际上并不使用按名称调用,而按名称调用可确保参数中的名称解析到适当的作用域。对文本替换的依赖使 CPP 成为一个不卫生的宏系统。其他系统(如 Scheme 的系统)是卫生的,它们为宏引入的名称创建了单独的作用域,并确保参数不会被捕获。

Scheme Macros
#

作为 R5RS Scheme 规范的一部分,宏系统是卫生的。宏由 define-syntax、let-syntax 或 letrec-syntax 中的一种形式引入,并将给定的名称与宏绑定。例如,下面是 let 作为宏的定义:

(define-syntax let
  (syntax-rules ()
    ((let ((name val) ...)
       body1 body2 ...
     )
     ((lambda (name ...)
        body1 body2 ...
      )
      val ...
     )
    )
  )
)

syntax-rules 指定了宏转换的规则。第一个参数是规则模式与输入之间必须匹配的字面形式列表。例如,cond 形式中的 else 标识符。但在这种情况下,没有字面意义。syntax-rules 的其余参数指定了转换。转换的第一项是输入模式,第二项是输出模式。...的作用类似于克莱因星,将前一项与输入中出现的零次或多次相匹配。输入模式中出现但不在字面量列表中的名称,除了作为宏名称的第一项,都是与输入元素相匹配的卫生变量。这些变量可以在输出模式中引用,以指定如何构造输出。

在全局环境中评估上述表达式时,会将 let 名称与一个转换为 lambda 的宏绑定。

宏主体引入的标识符保证避免与其他标识符冲突,解释器通常会重命名标识符以避免冲突。下面是 swap 宏的定义:

(define-syntax swap
  (syntax-rules ()
    ((swap a b)
     (let ((tmp b))
       (set! b a)
       (set! a tmp)
     )
    )
  )
)

这就将 swap 的使用转化为一个表达式,通过临时变量 tmp 交换两个参数。因此

> (define x 3)
> (define y 4)
> (swap x y)
> x
4
> y
3

不过,与 CPP 宏不同的是,swap 宏引入的 tmp 与其他任何 tmp 都是不同的:

> (define tmp 5)
> (swap x tmp)
> x
5
> tmp
4

因为宏在 Scheme 中是卫生的,所以我们会得到预期的行为。

为了支持宏,Scheme 解释器的评估过程会像往常一样评估列表中的第一个项目。如果评估结果是一个宏,那么解释器将对列表的其余部分执行宏扩展,而不会首先评估参数。扩展引入的任何名称都与其他名称置于不同的作用域。扩展后,解释器会对扩展结果重复求值过程,因此,如果最终结果是一个 let 表达式 (如上文 swap 中的表达式),就会对该表达式进行求值。

一个宏定义可以指定多个模式规则。再加上扩展的结果会被求值,这使得宏定义可以递归,如下面的 let* 定义:

(define-syntax let*
  (syntax-rules ()
    ((let* ()
       body1 body2 ...
     )
     (let ()
       body1 body2 ...
     )
    )
    ((let* ((name1 val1) (name2 val2) ...)
       body1 body2 ...
     )
     (let ((name1 val1))
       (let* ((name2 val2) ...)
         body1 body2 ...
       )
     )
    )
  )
)

let* 没有绑定时,有一种基本模式,在这种情况下,它直接转化为一个 let。当至少有一个绑定时,也有一个递归模式,在这种情况下,let* 会转化为一个嵌套在 let 中的更简单的 let*。宏定义中的省略号 (...) 类似于正则表达式中的 Kleene 星号 (*),表示前一项可以匹配零次或多次。因此,具有单一绑定的 let* 与上述第二条模式规则相匹配,其中 (name2 val2) 匹配次数为零。

CPP Macros
#

我们再来看看 CPP 宏。尽管宏不卫生,但在涉及元编程的任务中却非常有用。

CPP 允许我们使用 #define 来定义两种类型的 macro,即 object-like macrofunction-like macroobject-like macro 是一种简单的文本替换,用一个文本序列替换另一个文本序列。在历史上,定义常量是一种常用的方法:

#define PI 3.1415926535

int main() {
  cout << "pi = " << PI << endl;
  cout << "tau = " << PI * 2 << endl;
}

在 C++ 中,更好的做法是使用 const 或 constexpr 定义常量。

function-like macro 接受参数 (如上文的 SWAP),并能将参数文本替换到替换文本中的特定位置。

使用 function-like macro 的一个更复杂的例子是对遵循相同模式的多段代码进行抽象定义。请看表示复数的类型定义:

struct Complex {
  double real;
  double imag;
};

ostream &operator<<(ostream &os, Complex c) {
  return os << "(" << c.real << "+" << c.imag << "i)";
}

假设除了上述重载的流插入操作符之外,我们还希望支持算术运算 +-*。这些运算的基本形式相同:

Complex operator <op>(Complex a, Complex b) {
  return Complex{ <expression for real>, <expression for imag> };
}

在这里,我们使用了 uniform initialization syntax 来初始化一个含有其成员值的 Complex。然后,我们可以编写一个 function-like macro 来抽象这个结构:

#define COMPLEX_OP(op, real_part, imag_part)    \
  Complex operator op(Complex a, Complex b) {   \
    return Complex{ real_part, imag_part };     \
  }

macro 的参数包括运算符、计算实部的表达式和计算虚部的表达式。我们可以使用下面的宏来定义运算:

COMPLEX_OP(+, a.real+b.real, a.imag+b.imag);
COMPLEX_OP(-, a.real-b.real, a.imag-b.imag);
COMPLEX_OP(*, a.real*b.real - a.imag*b.imag,
           a.imag*b.real + a.real*b.imag);

与我们最初的 SWAP 实现一样,尾部的分号是多余的,但却提高了可读性以及与语法高亮程序的交互性。使用 g++ -E 在预处理器中运行代码,我们可以得到(修改间距):

Complex operator +(Complex a, Complex b) {
  return Complex{ a.real+b.real, a.imag+b.imag };
};
Complex operator -(Complex a, Complex b) {
  return Complex{ a.real-b.real, a.imag-b.imag };
};
Complex operator *(Complex a, Complex b) {
  return Complex{ a.real*b.real - a.imag*b.imag,
                  a.imag*b.real + a.real*b.imag };
};

接下来,我们可以定义 Complexdouble 之间的运算。我们再次发现,这种操作具有特定的模式:

Complex operator <op>(<type1> a, <type2> b) {
  return <expr1> <op> <expr2>;
}

这里,<exprN> 是转换为 Complex 表示的相应参数。我们可以使用宏对其进行抽象:

#define REAL_OP(op, typeA, typeB, argA, argB)   \
  Complex operator op(typeA a, typeB b) {       \
    return argA op argB;                        \
  }

我们还可以定义一个宏,将 double 转换为 Complex

#define CONVERT(a)                              \
  (Complex{ a, 0 })

这样,我们就可以对操作进行如下定义:

REAL_OP(+, Complex, double, a, CONVERT(b));
REAL_OP(+, double, Complex, CONVERT(a), b);
REAL_OP(-, Complex, double, a, CONVERT(b));
REAL_OP(-, double, Complex, CONVERT(a), b);
REAL_OP(*, Complex, double, a, CONVERT(b));
REAL_OP(*, double, Complex, CONVERT(a), b);

通过预处理器运行,我们可以得到

Complex operator +(Complex a, double b) { return a + (Complex{ b, 0 }); };
Complex operator +(double a, Complex b) { return (Complex{ a, 0 }) + b; };
Complex operator -(Complex a, double b) { return a - (Complex{ b, 0 }); };
Complex operator -(double a, Complex b) { return (Complex{ a, 0 }) - b; };
Complex operator *(Complex a, double b) { return a * (Complex{ b, 0 }); };
Complex operator *(double a, Complex b) { return (Complex{ a, 0 }) * b; };

现在我们可以如下使用复数:

int main() {
  Complex c1{ 3, 4 };
  Complex c2{ -1, 2 };
  double d = 0.5;
  cout << c1 + c2 << endl;
  cout << c1 - c2 << endl;
  cout << c1 * c2 << endl;
  cout << c1 + d << endl;
  cout << c1 - d << endl;
  cout << c1 * d << endl;
  cout << d + c1 << endl;
  cout << d - c1 << endl;
  cout << d * c1 << endl;
}
(2+6i)
(4+2i)
(-11+2i)
(3.5+4i)
(2.5+4i)
(1.5+2i)
(3.5+4i)
(-2.5+-4i)
(1.5+2i)

Stringification and Concatenation
#

在使用宏时,将宏参数转换为字符串或将其与其他标记连接起来可能很有用。例如,假设我们要编写一个交互式应用程序,读取用户的输入并执行相应的操作。对于复数,目标函数可能如下:

Complex Complex_conjugate(Complex c) {
  return Complex{ c.real, -c.imag };
}

string Complex_polar(Complex c) {
  return "(" + to_string(sqrt(pow(c.real, 2) + pow(c.imag, 2))) +
    "," + to_string(atan(c.imag / c.real)) + ")";
}

应用程序会将用户输入与代表操作的字符串进行比较,调用相应的函数,并打印出结果。这就是常见的模式:

if (<input> == "<action>")
  cout << Complex_<action>(<value>) << endl;

在这里,我们既需要动作的字符串表示法,也需要将 Complex_ 标记与动作标记本身连接起来的能力。我们可以为这种模式定义如下宏:

#define ACTION(str, name, arg)                  \
  if (str == #name)                             \
    cout << Complex_ ## name(arg) << endl

标记前的 ## 是字符串化运算符,将标记转换为字符串。Complex_name 之间的 ## 是标记粘贴操作符,用于连接两边的标记。

这样,我们就可以编写如下应用代码:

Complex c1 { 3, 4 };
string s;
while (cin >> s) {
  ACTION(s, conjugate, c1);
  ACTION(s, polar, c1);
}

通过预处理器运行这个程序,我们就能得到想要的结果:

Complex c1 { 3, 4 };
string s;
while (cin >> s) {
  if (s == "conjugate") cout << Complex_conjugate(c1) << endl;
  if (s == "polar") cout << Complex_polar(c1) << endl;
}

The Macro Namespace
#

使用 CPP 宏的一个缺陷是,它们不包含在任何特定的命名空间中。事实上,只要定义了宏,它就能替换任何符合条件的标识符,无论该标识符位于何处。因此,定义一个宏就相当于让一个特定的标识符充当保留关键字,程序员无法使用。这也是为什么常量通常最好定义为变量,限定为 const 或 constexpr,而不是类似对象的宏的原因之一。

为避免污染全局命名空间,使用了几种约定。

  • 第一种是在所有宏的前缀加上定义宏的库所特有的字符,以避免与其他库发生冲突。例如,我们的复数宏可以用 COMPLEX_ 作为前缀,以避免与其他宏或标识符冲突。
  • 第二种策略是在不再需要宏时,使用 #undef 预处理器指令取消对宏的定义。例如,在库代码的末尾,我们可能有如下代码:
#undef COMPLEX_OP
#undef REAL_OP
#undef CONVERT
#undef ACTION

这样,标识符就可以在以后的代码中用于其他目的。

Code Generation
#

虽然 macros 允许我们使用一种语言提供的宏设施生成代码,但在某些情况下,这种设施无法使用或不足以满足我们的目的。在这种情况下,在外部程序中用同一种语言或另一种语言编写代码生成器可能会比较方便。这种技术也称为 automatic programming

例如,R5RS Scheme 规范要求实现提供 car 和 cdr 的组合,最深可达四层。例如,(caar x) 应等同于 (car (car x))(caddar x) 应等同于 (car (cdr (cdr (car x))))。除了 carcdr 本身外,还需要提供 28 种组合,手工编写既繁琐又容易出错。相反,我们可以定义下面的 Python 脚本来生成一个 Scheme 库文件:

import itertools

def cadrify(seq):
    if len(seq):
        return '(c{0}r {1})'.format(seq[0], cadrify(seq[1:]))
    return 'x'

def defun(seq):
    return '(define (c{0}r x) {1})'.format(''.join(seq), cadrify(seq))

for i in range(2, 5):
    for seq in itertools.product(('a', 'd'), repeat=i):
        print(defun(seq))

cadrify() 函数是一个递归函数,它接收一个序列,如 ('a','d','a'),并使用第一个项目和序列其余部分的递归结果构造一个调用。在本例中,后者是 (cdr (car x)),因此结果是 (car (cdr (car x)))。基本情况是序列为空,只产生 x

defun() 函数接收一个序列,并用它为相应的组合构建定义。它调用 cadrify() 构建正文。对于序列 ('a','d','a'),结果是

(define (cadar x) (car (cdr (car x))))

最后,循环结束时会产生每个长度的 ad 的所有组合。它使用函数库中的 itertools.product() 函数来获得一个序列,该序列是元组 ('a','d') 的第 i 次幂。对于每个组合,它都会调用 defun() 生成该组合的函数。

(define (caar x) (car (car x)))
(define (cadr x) (car (cdr x)))
(define (cdar x) (cdr (car x)))
(define (cddr x) (cdr (cdr x)))
(define (caaar x) (car (car (car x))))
(define (caadr x) (car (car (cdr x))))
...
(define (cdddar x) (cdr (cdr (cdr (car x)))))
(define (cddddr x) (cdr (cdr (cdr (cdr x)))))

我们可以将生成的代码放入标准库中,由 Scheme 解释器加载。

Template Metaprogramming
#

Template metaprogramming 是一种在编译时使用模板生成源代码的技术,然后将源代码与程序的其他代码一起编译。它通常是指利用语言的模板实例化规则进行编译时执行的一种形式。Template metaprogramming 在 C++ 中最为常见,但也有少数其他语言可以使用。

C++ 中模板元编程的关键是 template specialization,它允许编写专门的定义来实例化带有特定参数的模板。例如,考虑一个包含静态值域的类模板,如果模板参数为 int,该值域为 true,否则为 false。我们可以如下编写通用模板:

template <class T>
struct is_int {
  static const bool value = false;
};

现在我们可以定义当参数为 int 时该模板的特殊化:

template <>
struct is_int<int> {
  static const bool value = true;
};

特化中的模板参数列表包含非特化参数。在上面的例子中,没有任何参数,所以是空的。然后,在模板名称之后,我们提供了实例化的全部参数集,在本例中只有 int。然后,我们提供实例化的其余定义。

现在,当我们使用模板时,如果模板参数与特化兼容,编译器就会使用特化,否则就会使用通用模板:

cout << is_int<double>::value << endl;
cout << is_int<int>::value << endl;

// output
0 1

模板特化使我们能够编写以模板参数为条件的代码。与递归实例化相结合,这使得模板实例化具有图灵完备性。模板不对可变变量进行编码,因此模板元编程实际上是函数式编程的一种形式。

Pairs
#

举个更复杂的例子,让我们定义可在编译时操作的 pairs 和 lists。这些结构中存储的元素将是任意类型。

在开始定义 pairs 之前,我们先构建一个报告机制,以便在编译时检查结果。我们将在编译器生成的错误信息中包含相关信息:

template <class A, int I>
struct report {
  static_assert(I < 0, "report");
};

为了简单起见,我们使用了一个 integer 模板参数,当然也可以使用类型对数字进行编码。在实例化 report 模板时,如果模板参数 I 为非负,static_assert 会引发错误。请看下面的内容:

report<int, 5> foo;

编译器会报错,指出是哪个实例导致 static_assert 失败。在 Clang 中,我们会得到类似下面的错误:

pair.cpp:64:3: error: static_assert failed "report"
  static_assert(I < 0, "report");
  ^             ~~~~~
pair.cpp:67:16: note: in instantiation of template class 'report<int, 5>'
      requested here
report<int, 5> foo;

使用 GCC,错误如下:

pair.cpp: In instantiation of 'struct report<int, 5>':
pair.cpp:67:16:   required from here
main.cpp:64:3: error: static assertion failed: report
   static_assert(I < 0, "report");
   ^

两个编译器都报告了相关信息,即报告模板的参数是 int 和 5。

这样,我们就可以定义 pair 模板如下:

template <class First, class Second>
struct pair {
  using car = First;
  using cdr = Second;
};

在 template 中,我们定义了类型别名 carcdr,用于指代配对的第一项和第二项。因此,pair<int, double>::carint 的别名,而 pair<int, double>::cdrdouble 的别名。

我们还可以定义类型别名,以便从数据对中提取第一项和第二项:

template <class Pair>
using car_t = typename Pair::car;
template <class Pair>
using cdr_t = typename Pair::cdr;

Pair::carPair::cdr 之前需要使用 typename 关键字,因为我们使用的是嵌套类型,其外层类型依赖于模板参数。在这种情况下,C++ 无法确定我们命名的是一个类型而不是一个值,因此 typename 关键字明确表示这是一个类型。使用上述别名,car_t<pair<int, double>>int 的别名,而 cdr_t<pair<int, double>>double 的别名。

为了表示递归列表,我们需要一个空列表的表示方法:

struct nil {
};

现在我们可以定义一个模板来判断一个列表是否为空,这个列表可以用空列表 nil 或以 nil 结尾的对序列来表示。我们定义了一个通用模板,然后针对以 nil 作为参数的情况定义了一个特殊化模板:

template <class List>
struct is_empty {
  static const bool value = false;
};

template <>
struct is_empty<nil> {
  static const bool value = true;
};

为了在编译时使用 value,它必须是一个 compile-time constant,我们可以将其设置为 staticconst,并使用编译时常量对其进行初始化。在 C++14 中,我们还可以定义全局变量模板(global variable templates)来编码 list 的长度:

template <class List>
const bool is_empty_v = is_empty<List>::value;

is_empty_v<nil> 的值为 true,而 is_empty<pair<int, nil>> 的值为 false。这样,我们就可以在编译时确定列表是否为空:

using x = pair<char, pair<int, pair<double, nil>>>;
using y = pair<float, pair<bool, nil>>;
using z = nil;
report<x, is_empty_v<x>> a;
report<y, is_empty_v<y>> b;
report<z, is_empty_v<z>> c;

在这里,我们为列表引入了类型别名,作为编译时不可变的变量。然后,我们用类型和是否为空实例化报告。这样,GCC 会给出以下错误信息:

pair.cpp: In instantiation of 'struct report<pair<char, pair<int,
  pair<double, nil> > >, 0>':
pair.cpp:82:28:   required from here
pair.cpp:73:3: error: static assertion failed: report
   static_assert(I < 0, "report");
   ^~~~~~~~~~~~~
pair.cpp: In instantiation of 'struct report<pair<float, pair<bool,
  nil> >, 0>':
pair.cpp:83:28:   required from here
pair.cpp:73:3: error: static assertion failed: report
pair.cpp: In instantiation of 'struct report<nil, 1>':
pair.cpp:84:28:   required from here
pair.cpp:73:3: error: static assertion failed: report

检查报告的整数参数,我们会发现列表 pair<char、pair<int、pair<double、nil>>pair<float、pair<bool、nil>> 不是空的,但列表 nil 是空的。

我们可以使用递归计算列表的长度:

template <class List>
struct length {
  static const int value = length<cdr_t<List>>::value + 1;
};

template <>
struct length<nil> {
  static const int value = 0;
};

template <class List>
const int length_v = length<List>::value;

在这里,我们使用的是 length 结构递归实例化的值。由于 value 是通过由编译时常量之间的运算组成的表达式初始化的,因此它也是一个编译时常量。与 is_empty_v 一样,我们定义了一个变量模板 length_v 来对结果进行编码。我们可以计算并报告 x 类型别名的长度:

report<x, length_v<x>> d;

报告的第一个参数是任意的,因为我们只关心第二个参数,所以我们只传递 x 本身。我们得到:

pair.cpp: In instantiation of 'struct report<pair<char, pair<int,
  pair<double, nil> > >, 3>':
pair.cpp:85:26:   required from here
pair.cpp:73:3: error: static assertion failed: report

相关信息是长度为 3。

我们还可以定义更复杂的列表操作。例如,我们可以将列表倒转如下:

template <class List, class SoFar>
struct reverse_helper {
  using type =
    typename reverse_helper<cdr_t<List>,
                            pair<car_t<List>, SoFar>>::type;
};

template <class SoFar>
struct reverse_helper<nil, SoFar> {
  using type = SoFar;
};

template <class List>
using reverse_t = typename reverse_helper<List, nil>::type;

在这里,我们使用一个辅助模板来执行反转,其中第一个模板参数是剩余列表,第二个模板参数是到目前为止的反转列表。在每一步中,我们以 pair<car_t<List>, SoFar> 的形式计算新的部分结果,将剩余列表中的第一个项目添加到前一个部分结果的前面。然后 cdr_t<List> 是除去第一个项目的剩余列表。

递归的基本情况是剩余列表为 nil,在这种情况下,最终结果与部分结果相同。我们使用部分类模板特化来实现这一点,它允许我们只特化类模板的部分参数。在 reverse_helper 中,我们对第一个参数进行了特殊化,这样,任何第一个参数为 nilreverse_helper 实例都将使用特殊化。特化保留了一个模板参数,该参数包含在参数列表中。完整的参数列表会出现在模板名称之后,包括特化和未特化的参数。

RESOURCES
#