奇怪的模板编译错误:这是 g++ 错误、Clang 错误还是...?

Strange template compile errors: is this a g++ bug, a Clang bug, or...?

我 运行 在一些复杂的 C++ 模板代码中出现编译错误,我已将其简化如下:


struct MyOptions
{
    static const size_t maxArray = 2;
    static const uint maxIdx = 8;
};

class OtherClass
{
    uint num;
  public:
    OtherClass(uint val) : num(val)
    {
    }
    void OtherCall(const char *varName, uint arraySize)
    {
        std::cout << '#' << num << ": " << varName << '[' << arraySize << ']' << std::endl;
    }
    template <class OPTS_> inline void OtherMethod(const char *varName)
    {
        OtherCall(varName, OPTS_::maxIdx);
    }
};

template <size_t COUNT_> class ConstArray
{
    OtherClass *other[COUNT_];
  public:
    ConstArray(OtherClass *o1, OtherClass *o2) // Just sample logic, shouldn't hard-code 2 elements
    {
        other[0] = o1;
        other[1] = o2;
    }
    inline OtherClass *operator[](size_t idx) const
    {
        return other[idx];  // Array itself not changeable by caller
    }
};

template <class OPTS_> class MyClass
{
    ConstArray<OPTS_::maxArray> others1;
    ConstArray<2> others2;
  public:
    MyClass(OtherClass *o1, OtherClass *o2) : others1(o1, o2), others2(o1, o2)
    {   // Just test code to initialize the ConstArray<> members
    }
    inline void PrintInfo(uint idx, const char *varName)
    {
        OtherClass *other1Ptr = others1[idx];
        other1Ptr->OtherMethod<OPTS_>(varName);             // This works
        others1[idx]->OtherMethod<OPTS_>(varName);          // This FAILS!!
        others2[idx]->OtherMethod<OPTS_>(varName);          // This works
    }
};

int main(int argc, char *argv[])
{
    OtherClass a(9), b(42);
    MyClass<MyOptions> mine(&a, &b);
    mine.PrintInfo(1, "foo");
    return 0;
}

上面 "This FAILS!!" 行的 g++ 5.4.0 中的错误消息是

error: expected primary-expression before ‘>’ token
         others1[idx]->OtherMethod<OPTS_>(varName);          // This FAILS!!
                                        ^

然而,很明显,当我使用临时 other1Ptr = others1[idx] 时,相同的逻辑编译得很好,分为 2 个语句,这让我相信这是一个 g++ 错误。

但我使用在线编译器在 Clang 中进行了尝试,得到了不同的(且相互冲突的)错误:

error: use 'template' keyword to treat 'OtherMethod' as a dependent template name
        others1[idx]->OtherMethod<OPTS_>(varName);  // This fails
                      ^
                      template 
error: use 'template' keyword to treat 'OtherMethod' as a dependent template name
        others2[idx]->OtherMethod<OPTS_>(varName);  // This works
                      ^
                      template 
2 errors generated.

所以 Clang 告诉我 others1[idx]->OtherMethods<>() 行实际上出了什么问题,并且还告诉我在 g++ 中工作的 others2[idx]->OtherMethod<>() 行实际上是错误的!

果然,如果我更改 PrintInfo() 代码,它可以在 Clang 中正常编译:

    inline void PrintInfo(uint idx, const char *varName)
    {
        OtherClass *other1Ptr = others1[idx];
        other1Ptr->OtherMethod<OPTS_>(varName);             // This works
//        others1[idx]->OtherMethod<OPTS_>(varName);          // This FAILS!!
        others1[idx]->template OtherMethod<OPTS_>(varName); // This works
//        others2[idx]->OtherMethod<OPTS_>(varName);          // This works ONLY IN g++!
        others2[idx]->template OtherMethod<OPTS_>(varName); // This works
    }

而且这段代码在 g++ 中也编译得很好,所以它似乎是正确的行为。

然而正如我们已经看到的,g++ 也接受了

        others2[idx]->OtherMethod<OPTS_>(varName);          // This works ONLY IN g++!

那是 g++ 中的错误吗?还是 Clang 对这个逻辑过于严格了?解决方法是将 others1[idx]->OtherMethod<>() 行分成两部分(使用临时变量)实际上是正确的,还是应该以某种方式也使用 "template" 关键字?

再看错误信息:

error: use 'template' keyword to treat 'OtherMethod' as a dependent template name
        others1[idx]->OtherMethod<OPTS_>(varName);  // This fails

它告诉你这样做:

        others1[idx]->template OtherMethod<OPTS_>(varName);
        others2[idx]->template OtherMethod<OPTS_>(varName);

然后就可以了:)

编译器只是请求帮助区分 OtherMethod 的两个重载 (即模板或非模板方法)

请具体查看 this link 部分 "The template disambiguator for dependent names"。

编辑:原则上我同意@ascheper 的回答。但是,上面的 link 还指出 "As is the case with typename, the template prefix is allowed even if the name is not dependent or the use does not appear in the scope of a template (since C++11)." 因此,在您的特定情况下,这是允许的,但不是必需的。

我认为 g++ 在这里是正确的(尽管 clang++ 有更好的错误消息措辞),而 clang++ 拒绝 others2[idx]->OtherMethod<OPTS_>(varName); 语句是不正确的。尽管正如@walnut 在评论中指出的那样,clang 的最新源代码正确地接受了该声明。

C++17 [temp.names]/4:

在某些情况下对 template 的要求

In a qualified-id used as the name in a typename-specifier, elaborated-type-specifier, using-declaration, or class-or-decltype, an optional keyword template appearing at the top level is ignored. In these contexts, a < token is always assumed to introduce a template-argument-list. In all other contexts, when naming a template specialization of a member of an unknown specialization ([temp.dep.type]), the member template name shall be prefixed by the keyword template.

在此处的所有相关案例中,成员模板名称 OtherMethod 出现在使用 -> 标记的 class 成员访问表达式中。 OtherMethod 成员不是 "a member of the current instantiation",因为在代码的上下文中只有类型 MyClass<OPTS_> "is the current instantiation"。因此,根据 [temp.res]/(6.3),仅当对象表达式的类型依赖时,名称 OtherMethod 才为 "a member of an unknown specialization"。

语句 1:

others1[idx]->OtherMethod<OPTS_>(varName);

对象表达式为*(others1[idx])others1 是具有依赖类型 ConstArray<OPTS_::maxArray> 的当前实例化的成员,因此 others1 是类型依赖的 ([temp.dep.expr]/(3.1)) and others1[idx] and *(others1[idx]) are also type-dependent ([temp.dep.expr]/1)。 template 关键字是必需的。

语句 2:

others2[idx]->OtherMethod<OPTS_>(varName);

这一次,others2 是当前实例化的成员,但具有非依赖类型 ConstArray<2>。表达式 idx 命名了一个非类型模板参数,因此它依赖于值 ([type.dep.constexpr]/(2.2)) 但它不依赖于类型(它的类型总是 uint,不管它是什么) .所以*(others2[idx])是不依赖类型的,OtherMethod.

之前的template关键字是可选的

语句 3:

other1Ptr->OtherMethod<OPTS_>(varName);

对象表达式为*other1Ptrother1Ptr 具有类型 OtherClass*,因此 other1Ptr*other1Ptr 都不依赖于类型,关键字 template 是可选的。

将语句一分为二并不像看起来的那样"equivalent"。在声明中

OtherClass *other1Ptr = others1[idx];

初始化表达式others1[idx]如上所述是依赖于类型的,但是你已经将特定的非依赖类型OtherClass*赋给了临时变量。如果你已经声明它 auto other1Ptr,或 auto *other1Ptr,等等,那么变量名将是类型相关的。使用显式类型OtherClass*,这两个语句加起来可能更类似于

static_cast<OtherClass*>(others1[idx])->OtherMethod<OPTS_>(varName);

这也是有效的。 (这也不完全等价,因为 static_cast 允许某些隐式转换不允许的转换。)