Во время работы наша команда постоянно сталкивается с некоторыми особенностями языка, которые могут быть неизвестны рядовому C++ программисту. В этой статье мы расскажем о том, как работает, казалось бы, обыденная вещь – forward-декларации классов.

Предыстория
Для начала ответьте на вопрос: скомпилируется ли следующий код?
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(); }
Если вас смутила декларация указателя pTDStretch, то поздравляю – она смутила и меня. Но перед тем, как разобраться в этом поведении, предлагаю ознакомиться с предысторией того, как мы отыскали сей интересный артефакт.
Этот код (в несокращённом варианте) мы встретили во время переработки системы типов в PVS-Studio. Перед нами появился вот такой "дифф" – это различие в срабатывании, которое возникает между стабильной и тестовой версией анализатора при прогоне на тестовой базе:
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; }
Проблема была в том, что мы не могли связать вызов функции getInput с её декларацией. В процессе отладки выяснилось, что мы не можем найти объявление класса TDStretch. И действительно – при просмотре кода его не найти! Но откуда мы находили декларацию этой функции раньше, до переработок? И почему мы находили её во внешнем классе? Должно быть, это какая-то ошибка, и этот код на самом деле не должен компилироваться.
Упрощаю пример для Compiler Explorer и проверяю на компилируемость... Стоп, что?!? Оно компилируется?!? Но почему? Спрашиваю об этом тимлида – он тоже в недоумении. Пришлось идти и раскапывать стандарт. В процессе совместных раскопок выяснилось, что это на самом деле ожидаемое поведение. Давайте посмотрим, что же лежит внутри сундука...

Ожидаемое поведение
Итак, смотрим в стандарт C++, чтобы понять, в каком месте должен быть объявлен класс. Оно определяется в разделе [basic.scope.pdecl] p7.
Если декларация имеет вид:
class Foo;
то объявление будет находиться в той области видимости, в которой находится декларация. Например:
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 { };
Иначе, если класс объявлен в параметрах или возвращаемом значении функции, то класс будет находиться в namespace, где объявлена функция. Например:
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 {};
Иначе класс будет находиться в ближайшем блоке или namespace. Например:
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' }; }
Для этого случая есть одно исключение – friend-декларации. Они на самом деле не внедряют никаких новых имён.
Более подробно правило описано в стандарте (см. ссылку выше).
В нашем случае используется последний пункт. Однако стоит дописать class TDStretch; в класс SoundTouch, то код компилироваться не будет.
Lookup
Стоит отметить ещё один важный пункт. Не всегда конструкция class Foo; декларирует новый класс. Она может ссылаться на уже объявленный класс, причём не обязательно он должен находиться в текущей области видимости.
Данное поведение регламентируется стандартом в разделе [basic.lookup.elab] под пунктом 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].
Изначально, когда компилятор встречает данную конструкцию, он выполняет unqualified lookup указанного имени. Если имя было найдено, то данная конструкция ассоциируется с найденной декларацией.
Иначе, если имя найдено не было, оно декларируется по правилам, которые мы рассмотрели в предыдущем разделе. Например:
struct Foo { class Bar *ptr; // declaration of class '::Bar' }; namespace Baz { class Bar *anotherPtr; // uses previously-declared class '::Bar' }
Заключение
В этой статье мы рассмотрели интересную и неочевидную особенность языка. Надеемся, что она оказалась полезной и поможет вам в чтении и написании кода. А те, кто ответил, что код в первом примере – компилируется, можете считать себя гуру C++!
Если вас интересуют и другие тонкости языка C++, то приглашаю в наш блог. Вот несколько интересных технических статей:
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Larin. C++ subtleties: so, you've declared a class....
