2

I ran into a compile error in some complex, C++ template code, which I've simplified as follows:


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;
}

The error message in g++ 5.4.0 for the "This FAILS!!" line, above, was

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

And yet, obviously when I used the temporary other1Ptr = others1[idx], the same logic compiled just fine split into 2 statements, which was leading me to believe it was a g++ bug.

But I used an online compiler to try it in Clang, and got different (and conflicting) errors:

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.

So Clang tells me what's actually wrong with the others1[idx]->OtherMethods<>() line, and additionally informs me that the others2[idx]->OtherMethod<>() line that worked in g++ is actually wrong!

Sure enough, if I change the PrintInfo() code, it compiles fine in 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
    }

And this code also compiles fine in g++, so it seems it is the correct behavior.

Yet as we already saw, g++ also accepted

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

So is that a bug in g++? Or is Clang being too strict for this logic? And is the workaround, splitting the others1[idx]->OtherMethod<>() line into two pieces (with a temporary variable) actually correct, or should it be using the "template" keyword somehow also?

barnabas
  • 95
  • 8
  • Did you try declaring `maxArray` as `constexpr` instead of `const`? – Sam Varshavchik Mar 05 '20 at 03:41
  • No; I can try it, and suspect it might make the `ConstArray` case work like the `ConstArray<2>` in g++. Though that seems to be tangential to the question at this point, given the Clang behavior. Edit: no, it doesn't change g++ behavior. – barnabas Mar 05 '20 at 03:47
  • 1
    There are some contexts where `template` is optional, see https://stackoverflow.com/questions/610245/where-and-why-do-i-have-to-put-the-template-and-typename-keywords – M.M Mar 05 '20 at 04:28
  • Simpler code showing the same difference: https://godbolt.org/z/DfRiv6 – aschepler Mar 05 '20 at 04:29
  • @aschepler the question has two cases it's asking about (one where an intermediate pointer is defined) – M.M Mar 05 '20 at 04:36
  • @M.M Yes, but the intermediate pointer use doesn't have a different result between compilers. – aschepler Mar 05 '20 at 04:37
  • @M.M I didn't see the optional `template` cases in that post, can you point out which section? – barnabas Mar 05 '20 at 04:47
  • @barnabas there are examples in both the answer by Filip Roseen and Johannes Schaub – M.M Mar 05 '20 at 04:49
  • @aschepler Yes, originally I oversimplified, and had to keep adding complexity back until the bug was triggered (I originally suspected something in the behavior of the custom operator[] because of the cases that worked). – barnabas Mar 05 '20 at 04:50
  • 1
    I guess this was a Clang bug. Current trunk Clang agrees with GCC. – walnut Mar 05 '20 at 05:11
  • @walnut Yep, seems that way. Though I'd take Clang's lucid error message over g++'s opaque gobbledygook any day. – barnabas Mar 05 '20 at 07:07

3 Answers3

3

I think g++ is correct here (though clang++ has a better error message wording), and clang++ was incorrect to reject the others2[idx]->OtherMethod<OPTS_>(varName); statement. Though as @walnut notes in a comment, the most recent source code for clang correctly accepts that statement.

The requirement for template in some cases like this is in C++17 [temp.names]/4:

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.

In all the relevant cases here, the member template name OtherMethod appears in a class member access expression using the -> token. The OtherMethod member is not "a member of the current instantiation", since in the code's context only the type MyClass<OPTS_> "is the current instantiation". So by [temp.res]/(6.3), the name OtherMethod is "a member of an unknown specialization" only if the type of the object expression is dependent.

Statement 1:

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

The object expression is *(others1[idx]). others1 is a member of the current instantiation with dependent type ConstArray<OPTS_::maxArray>, so others1 is type-dependent ([temp.dep.expr]/(3.1)) and others1[idx] and *(others1[idx]) are also type-dependent ([temp.dep.expr]/1). The template keyword is required.

Statement 2:

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

This time, others2 is a member of the current instantiation but has non-dependent type ConstArray<2>. The expression idx names a non-type template parameter, so it is value-dependent ([type.dep.constexpr]/(2.2)) but it is not type-dependent (its type is always uint, whatever that is). So *(others2[idx]) is not type-dependent, and the template keyword is optional before OtherMethod.

Statement 3:

other1Ptr->OtherMethod<OPTS_>(varName);

The object expression is *other1Ptr. other1Ptr has type OtherClass*, so neither other1Ptr nor *other1Ptr is type-dependent, and the keyword template is optional.

Splitting the statement into two isn't as "equivalent" as it might look. In the declaration

OtherClass *other1Ptr = others1[idx];

the initializer expression others1[idx] is type-dependent as explained above, but you have given the specific non-dependent type OtherClass* to the temporary variable. If you had declared it auto other1Ptr, or auto *other1Ptr, or etc., then the variable name would be type-dependent. Using explicit type OtherClass*, the two statements together are perhaps more similar to

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

which would also be valid. (This isn't entirely equivalent either, since a static_cast would allow some conversions which an implicit conversion won't.)

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Wow, I had to try your suggested `OtherClass *other1Ptr = others1[idx];` `other1Ptr->template OtherMethod(varName);` and sure enough g++ does accept the optional template specifier! (Not that I doubted you, but in my experience the language is so picky about using typename exactly and only when necessary that it's refreshing to see them ignoring a "clarificational" `template` specifier when it's not strictly required, rather than outputting "error, remove the keyword" messages). – barnabas Mar 05 '20 at 06:38
  • (Admittedly a huge portion of that experience was formed prior to C++11 adoption at my prior companies) – barnabas Mar 05 '20 at 06:58
  • This answer makes a murky area really clear. (I'd upvote you as well but this account is too new to register upvotes.) – barnabas Mar 05 '20 at 07:01
0

Please have a look at this link specifically under the section "The template disambiguator for dependent names".

Edit: In principly I agree with the answer of @ascheper. However, the same link above states also that "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)." So, it is allowed but not required in your specific case.

Thomas Lang
  • 770
  • 1
  • 7
  • 17
  • The `template` keyword is not required in the specific case that OP asks about at the end. See the answer by aschepler. I suggest you add a more concrete reasoning if you disagree with the conclusion of that answer. – walnut Mar 05 '20 at 06:11
  • Interesting, it seems that in C++11, typename supposedly isn't as picky about exact usage as my previous experience suggests. Seems like I have contrary cases even in C++14 to this, if I look through my current development; I did do huge template projects prior to C++11, so admittedly much of my impression may no longer be accurate. I've always been bothered by cases where typename is required when using a typedef'd type, which has always been a pet peeve of mine, but now I see from recent code that those are now only cases where the typedef'd name itself contains types. – barnabas Mar 05 '20 at 06:56
-3

Look again at the error message:

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

It's telling you to do this:

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

and then it works :)

The compiler is just asking for help to distinguish between the two overloads of OtherMethod (i.e. the template, or non template method)

robthebloke
  • 9,331
  • 9
  • 12
  • 1
    Yes, I already showed that behavior in my code samples. The question is whether the `others2[idx]->OtherMethod(varName);` line, which g++ accepts, actually should require the "template" keyword (a g++ bug), or whether Clang is requiring it unnecessarily (a Clang bug, though I doubt it's this case). Or I suppose "template" could be optional here and they're both right, but I sort of doubt that... – barnabas Mar 05 '20 at 03:53
  • 2
    Your last sentence is bogus, `OtherMethod` is not overloaded (it's a function template) – M.M Mar 05 '20 at 04:43