Our team constantly encounters some C++ features that may be unknown to some developers. In this article, we're going to learn how a seemingly typical feature — class forward declarations — works.
Let's start with a question: does the following code compile?
namespace soundtouch
{
class SoundTouch
{
public:
class TDStretch *pTDStretch;
};
class TDStretch
{
public:
void *getInput() { return nullptr; }
};
}
auto bbb = soundtouch::TDStretch {};
int main(int argc, char** argv)
{
soundtouch::SoundTouch st;
st.pTDStretch = &bbb;
return !!st.pTDStretch->getInput();
}
Noticed the pTDStretch pointer? Does this declaration look strange to you? Congratulations! That's what confused me too. But before we figure out what's going on with this behavior, let me tell you the backstory of how we found this interesting artifact.
We came across this code (in its full version) when we were reworking the type system in PVS-Studio. We got the following diff (the difference in warnings that occurs between the stable and test versions of the analyzer when running on a test base):
V547 Expression 'psp' is always true. - MISSING IN CURRENT SoundTouch.cpp 493
class SoundTouch : public FIFOProcessor
{
private:
/// Time-stretch class instance
class TDStretch *pTDStretch;
};
/// Class that does the time-stretch (tempo change) effect for the processed
/// sound.
class TDStretch : public FIFOProcessor
{
public:
/// Returns the input buffer object
FIFOSamplePipe *getInput() { return &inputBuffer; };
};
/// Returns number of samples currently unprocessed.
uint SoundTouch::numUnprocessedSamples() const
{
FIFOSamplePipe * psp;
if (pTDStretch)
{
psp = pTDStretch->getInput();
if (psp) // <=
{
return psp->numSamples();
}
}
return 0;
}
The problem was that we couldn't bind the getInput function call with its declaration. During debugging, it turned out that we couldn't find the TDStretch class declaration. Actually, it's really impossible to find it when reviewing the code! But how did we find the declaration of this function with the previous type system? Why did we find it in the outer class? It must be some kind of error, and this code shouldn't actually compile.
So, I decided: "I need to simplify the example for Compiler Explorer and check whether it compiles... Wait, what?!? Is it really compiling?!? But why?" Then I thought: "OK... perhaps my team lead knows what's going on here". Unfortunately, he was confused too. So, we had to dive into the C++ standard. While "diving", we found out that this was actually the expected behavior. Well, let's see what is inside the treasure chest...
So, let's open the C++ standard to learn where the class should be declared. It is defined in [basic.scope.pdecl] p7.
If the declaration has the following form:
class Foo;
then the declaration will be in the same scope that contains the declaration. For example:
struct Foo; // declaration of class '::Foo'
// definition of previously-declared class '::Foo'
struct Foo
{
struct Bar; // declaration of class '::Foo::Bar'
};
// definition of previously-declared class '::Foo::Bar'
struct Foo::Bar
{
};
namespace Baz
{
struct Qux; // declaration of class '::Baz::Qux'
}
// definition of previously-declared class '::Baz::Qux'
struct Baz::Qux
{
};
Otherwise, if the class is declared in the parameters or the return value of the function, then the class will be in the namespace that contains the function declaration. For example:
void func(class Foo *p); // declaration of class '::Foo'
struct Bar
{
struct Baz *funcReturningClassPtr(); // declaration of class '::Baz'
};
namespace Qux
{
struct Quux *anotherFunction(); // declaration of class '::Qux::Quux'
}
// definition of previously-declared class '::Baz'
struct Baz {};
// definition of previously-declared class '::Qux::Quux'
struct ::Qux::Quux {};
Otherwise, the class is declared in the smallest namespace or block scope that contains the declaration. For example:
struct Foo
{
class Bar *baz; // declaration of class '::Bar'
};
void func()
{
struct Baz *ptr; // declares local class 'Baz'
struct Baz {}; // definition of previously-declared class 'Baz'
}
namespace Qux
{
struct Baz
{
struct Quux *ptr; // declares class '::Qux::Quux'
};
}
There is one exception for this case — friend declarations. They don't actually introduce new names.
To get a more detailed explanation of the rule, check the standard (open the link above).
In our case, the last point is used. However, if we add class TDStretch; to the SoundTouch class, the code will not compile.
It's worth noting one more important thing. The class Foo; declaration statement does not always declare a new class. It can refer to an already declared class, and it does not necessarily have to be in the current scope.
This behavior is regulated by the standard in the [basic.lookup.elab] section under paragraph 2:
If the elaborated-type-specifier has no nested-name-specifier, and unless the elaborated-type-specifier appears in a declaration with the following form:
class-key attribute-specifier-seqopt identifier ;
the identifier is looked up according to [basic.lookup.unqual] but ignoring any non-type names that have been declared.
If the elaborated-type-specifier is introduced by the enum keyword and this lookup does not find a previously declared type-name, the elaborated-type-specifier is ill-formed.
If the elaborated-type-specifier is introduced by the class-key and this lookup does not find a previously declared type-name, or if the elaborated-type-specifier appears in a declaration with the form:
class-key attribute-specifier-seqopt identifier ;
the elaborated-type-specifier is a declaration that introduces the class-name as described in [basic.scope.pdecl].
Initially, when the compiler encounters this construct, it performs an unqualified lookup of the specified name. If the name was found, then this construct is associated with the found declaration.
Otherwise, if the name was not found, it is declared according to the rules that we discussed in the previous section. For example:
struct Foo
{
class Bar *ptr; // declaration of class '::Bar'
};
namespace Baz
{
class Bar *anotherPtr; // uses previously-declared class '::Bar'
}
In this article, we've discussed an interesting and not obvious feature of the language. We hope that it turned out to be useful and will help you in reading and writing code. By the way, if you answered that the code in the first example compiles, consider yourself a C++ guru!
If you are interested in other subtleties of C++, welcome to our blog. Here are some interesting tech articles: