今天要做某个功能,其中我想用一个单一参数的宏,来实现基类访问。毕竟两个参数的谁都会(我故意漏掉了末尾的分号,这样强制在使用时加上分号):
#define DECLARE_CLASS(this_class, base_class) \
typedef base_class MyBase; \
typedef this_class MyClass
对于普通的类,实现起来是这个样子:
#define DECLARE_ROOT_CLASS(class_name) \
typedef class_name MyClass
#define DECLARE_CLASS(class_name) \
typedef MyClass MyBase; \
typedef class_name MyClass
使用:
class Base {
DECLARE_ROOT_CLASS(Base);
};
class Derived : public Base {
DECLARE_CLASS(Derived);
};
很明显,这样利用 typedef
的覆盖规则,省去了再抄一遍基类名称的麻烦。另外,我用了老式的 typedef
而不是更现代的 using
,这在这里只是风格问题,对语义无影响。
对于模板类,我打算如法炮制。可是,问题出现了。
我的宏定义是这样的:
#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
处出错了。
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 所说的“第一遍解析的时候找不到”,指的是对模板类中符号的二次查找。这里由于 MyBase
(TBase
)是一个普通符号,而不是 type-dependent(SomeTemplate<T>::TBase
、TBase<T>
)也不是 value-dependent(SomeTemplate<N>::TBase
、TBase<N>
),所以不能通过一次扫描来确定合适的上下文。也就是 stat 说的:“(解析)模版的时候 TBase
是不知道是基类的 typedef
的”。
不过,我并不知道中文该叫什么,本文标题里的“待决名”是将语言切换到中文后显示的翻译。
下一个问题,VC++ 这里的行为是怎么回事。让我们看看文档和开发博客里是怎么说的。VC++ 默认遵照的不是标准行为,而是往里面加了点毒。在进行二次查找时,查找的位置错了。就从文章中的例子来看看:
#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# 的可选命名参数:
void Test() {
CallSomeMethod(param2 = "Param2Value", param1 = param1Value);
CallSomeMethod(param1 = param1Value, "Param2Value");
CallSomeMethod(param1Value, "Param2Value");
}
副作用就是会有少许污染。
在移植的时候,在变量上我使用了和原来代码相同的命名,其中就包括 xor
。结果编译器就报错了。
一查,才知道原来 C++ 还有一串运算符的替代表示法。在默认情况下,这些替代名称是启用的,除非使用选项关闭。以链接中的例子来说,这样的代码虽然看上去很奇怪,但也是可以正常编译的:
%: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:>);
%>
%>