今天要做某个功能,其中我想用一个单一参数的宏,来实现基类访问。毕竟两个参数的谁都会(我故意漏掉了末尾的分号,这样强制在使用时加上分号):
1 |
对于普通的类,实现起来是这个样子:
1 |
使用:
1 | class Base { |
很明显,这样利用 typedef
的覆盖规则,省去了再抄一遍基类名称的麻烦。另外,我用了老式的 typedef
而不是更现代的 using
,这在这里只是风格问题,对语义无影响。
对于模板类,我打算如法炮制。可是,问题出现了。
我的宏定义是这样的:
1 |
然而在编译的时候,在第4行产生了错误:
- (Clang 提示说,)VC++ 认为,
MyBase
等同于MyClass
(假设仍然使用后文的例子,则Derived::MyBase
等同于Derived
而不是期望的Base
); - Clang 和 GCC 认为,
MyBase
未定义。
我就不理解了,为什么是未定义呢?似乎以前 stat 给 UB(两个把 C++ 玩出花的大佬)讲过 VC++ 有符号名称查找(name lookup)上的错误,与标准的不符。但是,Clang 和 GCC 是怎么回事?这里的 VC++ 又是怎么回事?
所以我就问了 stat,如下的例子中为什么 TBase
处出错了。
1 | template<typename T1, typename T2> |
stat 想了一会儿,说:“TBase
是 non-dependent,第一遍解析的时候找不到。”
什么是 non-dependent 呢,就是 dependent 的反义词咯。那什么又是 dependent 呢?我就搜索了一下。全称应该叫做 dependent name,具体还可以分为 type-dependent 和 value-dependent。(不是 argument-dependent lookup!)
stat 所说的“第一遍解析的时候找不到”,指的是对模板类中符号的二次查找。这里由于 MyBase
(TBase
)是一个普通符号,而不是 type-dependent(SomeTemplate<T>::TBase
、TBase<T>
)也不是 value-dependent(SomeTemplate<N>::TBase
、TBase<N>
),所以不能通过一次扫描来确定合适的上下文。也就是 stat 说的:“(解析)模版的时候 TBase
是不知道是基类的 typedef
的”。
不过,我并不知道中文该叫什么,本文标题里的“待决名”是将语言切换到中文后显示的翻译。
下一个问题,VC++ 这里的行为是怎么回事。让我们看看文档和开发博客里是怎么说的。VC++ 默认遵照的不是标准行为,而是往里面加了点毒。在进行二次查找时,查找的位置错了。就从文章中的例子来看看:
1 |
|
标准行为是,在模板类/模板函数中使用的符号,其查找位置为定义处,也就是说,要查找这个符号,范围应该从开头至此处。“定义”指的是模板的定义。而 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 | void Test() { |
副作用就是会有少许污染。
在移植的时候,在变量上我使用了和原来代码相同的命名,其中就包括 xor
。结果编译器就报错了。
一查,才知道原来 C++ 还有一串运算符的替代表示法。在默认情况下,这些替代名称是启用的,除非使用选项关闭。以链接中的例子来说,这样的代码虽然看上去很奇怪,但也是可以正常编译的:
1 | %:include <iostream> |