28

In my C++ JSON library, I recently had a regression with GCC7. I stripped down the affected code and hope to understand the error.

The code

Consider this header myclass.hpp:

#pragma once

template <typename X>
struct A
{
    struct value_t
    {
        X array;
    };

    static A array()
    {
        return A();
    }

    friend bool operator<(const A& lhs, const A& rhs) noexcept
    {
        return lhs.val.array < rhs.val.array;
    }

    value_t val = {};  
};

As you see, I used the name "array" as member variable name in struct value_t, as name of a static function. I then included the header in the following file:

#include <array>
using std::array; // note this!
#include "myclass.hpp"

int main()
{}

The problem

The code compiles with GCC6 and Clang5 (using -std=c++11), but GCC7 reports:

In file included from example.cpp:3:0:
myclass.hpp: In function 'bool operator<(const A<X>&, const A<X>&)':
myclass.hpp:19:40: error: wrong number of template arguments (1, should be 2)
         return lhs.val.array < rhs.val.array;
                                        ^~~~~
In file included from example.cpp:1:0:
/usr/local/Cellar/gcc/7.1.0/include/c++/7.1.0/array:94:12: note: provided for 'template<class _Tp, long unsigned int _Nm> struct std::array'
     struct array
            ^~~~~
make: *** [all] Error 1

It seems as if the parser reads the "array" in lhs.val.array as std::array and treats the following < as the start of a template list.

The code can be compiled if I make any of the changes below:

  • Remove the using std::array; or move it behind #include "myclass.hpp".
  • Change return lhs.val.array < rhs.val.array; to return (lhs.val.array) < rhs.val.array;.

In addition, either compiler fails if I remove the static A array() function...

My questions

  • Is the code correct in the first place? Am I allowed to use "array" as a name even if I use using std::array;?
  • If the code is correct, is this a bug in GCC7?
Niels Lohmann
  • 2,054
  • 1
  • 24
  • 49
  • 1
    I would chose something more descriptive than "array" for a variable name. –  Jun 05 '17 at 11:32
  • 7
    It looks a lot like a GCC bug, but you can easily solve it doing this: `return (lhs.val.array) < rhs.val.array;`. [It compiles just fine with GCC7](https://wandbox.org/permlink/xmUQXD4gm2eOwos3) and GCC8 as well. – skypjack Jun 05 '17 at 11:36
  • I'd interpret it as a gcc 7 bug. However, if its not occurring with gcc 7.1, probably, no need for a bug report. – Peter Jun 05 '17 at 11:40
  • 1
    @Peter Reproducible up to GCC8 snapshot. – skypjack Jun 05 '17 at 11:40
  • The compilers fail if you *remove* the static member function? – Daniel Jour Jun 05 '17 at 12:17
  • 3
    [Simplified MCVE](https://godbolt.org/g/AajtFz) (nothing to do with std::array specifically) – M.M Jun 05 '17 at 12:23
  • @M.M To enforce your thesis, if you use `unordered_map` as a name, it fails both with GCC6 **and** clang5. Therefore nothing to do with the compiler too. – skypjack Jun 05 '17 at 12:34
  • [Here](https://wandbox.org/permlink/EKHaBMDi5jKr4rkJ) is an example that fails both with GCC6 and clang5 (still solved by enclosing everything between `(` and `)`). – skypjack Jun 05 '17 at 12:47
  • Also not related to operator-overloading or `friend` delayed compilation... same problem with `bool f(` instead of `friend bool operator<(`, and same problem if the function body is given out-of-line – M.M Jun 05 '17 at 12:52
  • 1
    Inside the function, `lhs.val` has type *unknown specialization* (since the type of `lhs` is a dependent type); there are a lot of restrictions on the use of *unknown specialization*, with violations being ill-formed NDR. I'm not yet sure if this specific code is such a violation though – M.M Jun 05 '17 at 13:05
  • @M.M Because of the fact that it works using a couple of parenthesis, I wouldn't say it's a violation. It's probably due to some rules about name resolution or whatever in case of dependent names. An interesting question anyway. – skypjack Jun 05 '17 at 13:10
  • The [temp.names]/4 says that *if there is a member template of the same name*, and you don't use the keyword `template`, the compiler must assume the name refers to a non-template. But I can't find anything talking about the case where there isn't a member template of the same name, but there might be due to it being of unknown specialization. (BTW this may be why the member function `array` makes a difference in some compilers) – M.M Jun 05 '17 at 13:23
  • @M.M Meanwhile I'm trying to find the reason for which surrounding parentheses make it works, but I'm miserly failing. :-( – skypjack Jun 05 '17 at 13:29
  • 1
    Well that one is simple, `(x)<` cannot be parsed as introducing a template (the `<` must immediately follow an id-expression to be a template) – M.M Jun 05 '17 at 13:32
  • @M.M Yeah, I know the _why_. I was trying to _explain_ it with a reference to the working draft. I'm not that dazed!! The hangover is gone with the weekend by now. :-D – skypjack Jun 05 '17 at 13:35
  • Almost certainly a compiler bug. Compiler is performing unqualified name lookup (basic.lookup), which finds `std::array` first, and since it sees the `<`, it is interpreting `lhs.val.array <` as an attempted template instantiation. When in fact what should be happening is that the compiler sees the postfix expression `lhs.val.` as class member access (expr.ref), so that the id expression `array` can be interpreted as a member of the class. +1 for Microsoft this time around as it doesn't have the same issue gcc or clang do. – AndyG Jun 05 '17 at 17:34
  • @AndyG Actually, the fact that Microsoft accepts something that GCC and clang reject makes me think that it's something​ to be rejected. :-D – skypjack Jun 05 '17 at 18:08
  • 3
    Compare [core issue 1835](https://wg21.link/cwg1835). – T.C. Jun 05 '17 at 18:45
  • @M.M Isn't `lhs.val` a _member of the current instantiation_? – cpplearner Jun 06 '17 at 02:35
  • @cpplearner I don't think so, there isn't even an instantiation here – M.M Jun 06 '17 at 05:36
  • It is not recommended to use global using statements in header files, and even worse, in front of an include statement !! Your case adds yet another very good argument against this kind of practice. What will happen if one of you cpp file has such a statement in front of an include, and another cpp doesn't? Tsk, tsk.... that's very naughty. – Michaël Roy Jun 13 '17 at 18:06

1 Answers1

4

I didn't have found anything that says the behavior you uncovered is OK, but I have found following, that might be asserting otherwise.

When the name of a member template specialization appears after . or -> in a postfix-expression or after a nested-name-specifier in a qualified-id, and the object expression of the postfix-expression is type-dependent or the nested-name-specifier in the qualified-id refers to a dependent type, but the name is not a member of the current instantiation (14.6.2.1), the member template name must be prefixed by the keyword template. Otherwise the name is assumed to name a non-template.

[ Example:
struct X {
template<std::size_t> X* alloc();
template<std::size_t> static X* adjust();
};
template<class T> void f(T* p) {
T* p1 = p->alloc<200>(); // ill-formed: < means less than
T* p2 = p->template alloc<200>(); // OK: < starts template argument list
T::adjust<100>(); // ill-formed: < means less than
T::template adjust<100>(); // OK: < starts template argument list
}
— end example ]

You can work around id as already suggested by putting compared elements in parentheses. It will break name array<

    return (lhs.val.array) < (rhs.val.array);

Let's simplify you code a little bit more and remove all includes that might be obscuring what is going on. I will start with original code that still doesn't compile.

#include <cstddef> // needed for size_t
//using std::array; brings following two lines into your code:
template< class T, std::size_t N >
struct array;

template <typename X>
struct A
{
    struct value_t { int array; };
    value_t val = {};  
    friend bool operator<(const A& lhs, const A& rhs) {
        return (lhs.val.array < rhs.val.array);
    }
};

And now let's move struct value_t { int array; }; outside of templated definition:

#include <cstddef> // needed for size_t
//using std::array; brings following two lines into your code:
template< class T, std::size_t N >
struct array;

struct value_t { int array; };

template <typename X>
struct A
{
    value_t val = {};  
    friend bool operator<(const A& lhs, const A& rhs) {
        return (lhs.val.array < rhs.val.array);
    }
};

So by including <array> into your code, you brought template array into your code as shown here. In version with value_t outside of template there is array<T> and member array. These are different things and thus without any conflict.
When you put value_t inside template, the compiler starts its attempts to expand what comes from that template. It tries to do that with member array which should not happen as specified in the standard.

Anyway, it looks like bug in GCC, because when it appears in expression lhs.val.array it should be treated as templated only when prefixed with keyword template lhs.val.template array<

And yes using same name in different contexts is perfectly correct unless it is one of the reserved words, which array is not. But be careful with this use of names. I find it at least confusing to use name array for variable holding single integer. The name already suggests there will be more than one.

Marek Vitek
  • 1,573
  • 9
  • 20