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

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

#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 所说的“第一遍解析的时候找不到”,指的是对模板类中符号的二次查找。这里由于 MyBaseTBase)是一个普通符号,而不是 type-dependent(SomeTemplate<T>::TBaseTBase<T>)也不是 value-dependent(SomeTemplate<N>::TBaseTBase<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:>);
    %>
%>
分享到 评论