Fully Qualified Out-of-Line Definitions

Recently I stumbled upon a piece of C++ code that fails to compile with Clang, although it works with gcc just fine. Since I was unable to divine at a glance what was going on, I created the following Minimal Complete Verifiable Example :

cpp
using test_t = int;
struct S { test_t f(); };

// The next line will error out on clang with:
// 'test_t' (aka 'int') is not a class, namespace, or enumeration
::test_t ::S::f() { return 0; }

Only three lines, and all of them are "obviously correct". So, lets play around a bit to figure out what really generates the error. The first variant does not change the results:

cpp
using test_t = int;
struct S { test_t f(); };

// It does not matter if test_t is fully qualified or not,
// it will error out this way as well
test_t ::S::f() { return 0; }

The next two variants fix the error, allowing the program to compile as intended:

cpp
struct S { test_t f(); };

// Using int directly instead of via the typedef works
int ::S::f() { return 0; }
cpp
using test_t = int;
struct S { test_t f(); };

// Using S instead of ::S fixes the problem as well
::test_t S::f() { return 0; }

Neither me, nor anyone I showed this piece of code at work was able to simply see what could be wrong about it. Additionally, gcc compiles it just fine and gives the expected result — surely that means this trips some kind of bug in Clang?

Clang is always right

As usual, Clang is right and gcc and I are wrong. In fact, even the error message is totally appropriate! To understand what is going on here, let me just rewrite the function definition a tiny bit:

cpp
using test_t = int;
struct S { test_t f(); };

// Just leaving out a blank space that is ignored anyway:
::test_t::S::f() { return 0 }

This suddenly looks like a constructor for a type ::test_t::S::f! Both variants are understood by C++ in the same way, as "scope operator, identifier 'test_t', scope operator, identifier 'S', scope operator, identifier 'f', …", meaning the rewrite did not change anything.

Alright, so Clang is correct, but what about the error message? Well, we are asking C++ to find a subscope of the type test_t. However, the only things that can be followed by the scope resolution operator are namespaces , class types and enumerations1 and int is neither — which is exactly what Clang told us in the very beginning.

How to get around this?

While we already saw to ways to get around this error, neither is really satisfying: Removing the type alias is not really an option2 and using relative instead of absolute scope resolution seems like admitting defeat, which brings out my obnoxious side. We can however borrow from the function pointer syntax to write it in a way that allows it to be parsed as intended:

cpp
using test_t = int;
struct S { test_t f(); };

// Yay, this works:
::test_t (::S::f)() { return 0; }

In the end, it is just one more counterintuitive peculiarity of the C++ syntax.

Footnotes

  1. One example for each:

    cpp
    // namespace scope resolution:
    namespace A { struct B { }; }
    int main() { A::B t; }
    
    cpp
    // class type scope resolution
    struct A { struct B { }; };
    int main() { A::B t; }
    
    cpp
    // enumeration type scope resolution:
    enum A { X, Y };
    int main() { auto t = A::X; }
    
  2. Not only might it not resolve to a built-in type, but there might be a real reason for using it in the first place. For example, in the original code, it was ::std::uint64_t, which resolves to different built-in types depending on the compiler, standard library and target architecture.