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:

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 change does not change a thing:

// 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:

// Using int directly instead of via the typedef works:  
int ::S::f() { return 0; }  
// 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 this piece of code. Additionaly, 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:

// 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 understand 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:

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:

  2. // namespace scope resolution:  
    namespace A { struct B { }; }  
    int main() { A::B t; }  
    
    // class type scope resolution  
    struct A { struct B { }; };  
    int main() { A::B t; }  
    
    // enumeration type scope resolution:  
    enum A { X, Y };  
    int main() { auto t = A::X; }  
    
  3. 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.

Daniel Schemmel

is currently employed at the Chair of Communication and Distributed Systems at RWTH Aachen University, where he researchs the testability of distributed systems. He can be reached at blog(at)gha.st.

Aachen, Germany, Terra, Sol, Milky Way, Laniakea SC https://gha.st/about/