C++ 模板类的非待决名查找

今天要做某个功能,其中我想用一个单一参数的宏,来实现基类访问。毕竟两个参数的谁都会(我故意漏掉了末尾的分号,这样强制在使用时加上分号):

1
2
3
#define DECLARE_CLASS(this_class, base_class) \
typedef base_class MyBase; \
typedef this_class MyClass

对于普通的类,实现起来是这个样子:

1
2
3
4
5
6
#define DECLARE_ROOT_CLASS(class_name) \
typedef class_name MyClass

#define DECLARE_CLASS(class_name) \
typedef MyClass MyBase; \
typedef class_name MyClass

使用:

1
2
3
4
5
6
7
class Base {
DECLARE_ROOT_CLASS(Base);
};

class Derived : public Base {
DECLARE_CLASS(Derived);
};

很明显,这样利用 typedef 的覆盖规则,省去了再抄一遍基类名称的麻烦。另外,我用了老式的 typedef 而不是更现代的 using,这在这里只是风格问题,对语义无影响。

对于模板类,我打算如法炮制。可是,问题出现了。

我的宏定义是这样的:

1
2
3
4
5
6
#define DECLARE_ROOT_CLASS_TEMPLATE(class_name, TArgs...) \
typedef class_name<TArgs> MyClass

#define DECLARE_CLASS_TEMPLATE(class_name, TArgs...) \
typedef MyClass MyBase; \
typedef class_name<TArgs> MyClass

然而在编译的时候,在第4行产生了错误:

  • (Clang 提示说,)VC++ 认为,MyBase 等同于 MyClass(假设仍然使用后文的例子,则 Derived::MyBase 等同于 Derived 而不是期望的 Base);
  • Clang 和 GCC 认为,MyBase 未定义。

我就不理解了,为什么是未定义呢?似乎以前 stat 给 UB(两个把 C++ 玩出花的大佬)讲过 VC++ 有符号名称查找(name lookup)上的错误,与标准的不符。但是,Clang 和 GCC 是怎么回事?这里的 VC++ 又是怎么回事?

所以我就问了 stat,如下的例子中为什么 TBase 处出错了。

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T1, typename T2>
class Base {
typedef Base<T1, T2> TBase;
typedef TBase TThis;
}

template<typename T1>
class Derived : public Base<T1, T1*> {
// 后注:在提问时我手写的;不过这样写在所有编译器中都不能通过编译,出问题的还是“typedef TThis TBase;”
typedef typename TBase::TThis TBase;
typedef Derived TThis;
}

stat 想了一会儿,说:“TBase 是 non-dependent,第一遍解析的时候找不到。”

什么是 non-dependent 呢,就是 dependent 的反义词咯。那什么又是 dependent 呢?我就搜索了一下。全称应该叫做 dependent name,具体还可以分为 type-dependent 和 value-dependent。(不是 argument-dependent lookup!)

stat 所说的“第一遍解析的时候找不到”,指的是对模板类中符号的二次查找。这里由于 MyBaseTBase)是一个普通符号,而不是 type-dependent(SomeTemplate<T>::TBaseTBase<T>)也不是 value-dependent(SomeTemplate<N>::TBaseTBase<N>),所以不能通过一次扫描来确定合适的上下文。也就是 stat 说的:“(解析)模版的时候 TBase 是不知道是基类的 typedef 的”。

不过,我并不知道中文该叫什么,本文标题里的“待决名”是将语言切换到中文后显示的翻译。

下一个问题,VC++ 这里的行为是怎么回事。让我们看看文档开发博客里是怎么说的。VC++ 默认遵照的不是标准行为,而是往里面加了点毒。在进行二次查找时,查找的位置错了。就从文章中的例子来看看:

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

void func(void*) {
std::puts("The call resolves to void*");
}

template<typename T>
void g(T x) {
func(0);
}

void func(int) {
std::puts("The call resolves to int");
}

int main() {
g(3.14);
}

标准行为是,在模板类/模板函数中使用的符号,其查找位置为定义处,也就是说,要查找这个符号,范围应该从开头至此处。“定义”指的是模板的定义。而 VC++ 的行为是,查找位置在实际使用处。

按照这么个逻辑,上面的例子结果就很明显了。

  • 在标准行为下:g()func(0) 的调用会被解析到 void func(void*)。因为在此时(直到 func(0))编译器只知道这个声明,而且隐式转换是可以接受的。
  • 在 VC++ 行为下,会被解析到 void func(int)。因为在此时(直到 g(3.14))编译器知道两个重载了的声明(void func(void*)void func(int),而其中根据参数类型决断,更合适的是后者。

同理,也就产生了上面的问题。在代码中,如果 MyBase 的使用晚于 MyClass,就会产生 MyClass 已经被重新 typedef 的情况,从而让 MyBase 指向错误的类型。当然,就算是顺序恰巧对了,在某些情况下(比如调用前出现了多种特化/偏特化),还有可能因为非标准行为而产生难以预料的后果。


另外偶然看到的一个有意思的“可选命名参数”实现思路代码。调用效果类似 C# 的可选命名参数:

1
2
3
4
5
void Test() {
CallSomeMethod(param2 = "Param2Value", param1 = param1Value);
CallSomeMethod(param1 = param1Value, "Param2Value");
CallSomeMethod(param1Value, "Param2Value");
}

副作用就是会有少许污染。


在移植的时候,在变量上我使用了和原来代码相同的命名,其中就包括 xor。结果编译器就报错了。

一查,才知道原来 C++ 还有一串运算符的替代表示法。在默认情况下,这些替代名称是启用的,除非使用选项关闭。以链接中的例子来说,这样的代码虽然看上去很奇怪,但也是可以正常编译的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
%:include <iostream>

struct X
<%
compl X() <%%> // 析构函数
X() <%%>
X(const X bitand) = delete; // 复制构造函数

bool operator not_eq(const X bitand other)
<%
return this not_eq bitand other;
%>
%>;

int main(int argc, char* argv<::>)
<%
// 带引用捕获的 lambda
auto greet = <:bitand:>(const char* name)
<%
std::cout << "Hello " << name
<< " from " << argv<:0:> << '\n';
%>;

if (argc > 1 and argv<:1:> not_eq nullptr) <%
greet(argv<:1:>);
%>
%>
分享到 评论