﻿# Тонкости C\+\+: итак, вы объявили класс…

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

![1031_class_lookup_ru/image1.png](https://import.viva64.com/docx/blog/1031_class_lookup_ru/image1.png)

## Предыстория

Для начала ответьте на вопрос: скомпилируется ли следующий код?

```cpp
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_, то поздравляю – она смутила и меня\. Но перед тем, как разобраться в этом поведении, предлагаю ознакомиться с предысторией того, как мы отыскали сей интересный артефакт\. 

Этот код \(в несокращённом варианте\) мы встретили во время [переработки системы типов](https://pvs-studio.ru/ru/blog/posts/cpp/0992/) в PVS\-Studio\. Перед нами появился вот такой "дифф" – это различие в срабатывании, которое возникает между стабильной и тестовой версией анализатора при прогоне на тестовой базе:

V547 Expression 'psp' is always true\. \- MISSING IN CURRENT SoundTouch\.cpp 493

```cpp
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](https://godbolt.org/z/c74E1Y696) и проверяю на компилируемость\.\.\. Стоп, что?\!? Оно компилируется?\!? Но почему? Спрашиваю об этом тимлида – он тоже в недоумении\. Пришлось идти и раскапывать стандарт\. В процессе совместных раскопок выяснилось, что это на самом деле ожидаемое поведение\. Давайте посмотрим, что же лежит внутри сундука\.\.\.

![1031_class_lookup_ru/image2.png](https://import.viva64.com/docx/blog/1031_class_lookup_ru/image2.png)

## Ожидаемое поведение

Итак, смотрим в стандарт C\+\+, чтобы понять, в каком месте должен быть объявлен класс\. Оно определяется в разделе [\[basic\.scope\.pdecl\] p7](https://timsong-cpp.github.io/cppwp/n4861/basic.scope.pdecl#7)\.

Если декларация имеет вид:

```cpp
class Foo;
```

то объявление будет находиться в той области видимости, в которой находится декларация\. [Например](https://godbolt.org/z/KqoWvxod6):

```cpp
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_, где объявлена функция\. [Например](https://godbolt.org/z/TxMW38Gzr):

```cpp
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_\. [Например](https://godbolt.org/z/EYG4MYavW):

```cpp
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\]](https://timsong-cpp.github.io/cppwp/n4861/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](https://en.cppreference.com/w/cpp/language/unqualified_lookup) указанного имени\. Если имя было найдено, то данная конструкция ассоциируется с найденной декларацией\.

Иначе, если имя найдено не было, оно декларируется по правилам, которые мы рассмотрели в предыдущем разделе\. Например:

```cpp
struct Foo
{
  class Bar *ptr; // declaration of class '::Bar'
};

namespace Baz
{
  class Bar *anotherPtr; // uses previously-declared class '::Bar'
}
```

## Заключение

В этой статье мы рассмотрели интересную и неочевидную особенность языка\. Надеемся, что она оказалась полезной и поможет вам в чтении и написании кода\. А те, кто ответил, что код в первом примере – компилируется, можете считать себя гуру C\+\+\! 

Если вас интересуют и другие тонкости языка C\+\+, то приглашаю в наш блог\. Вот несколько интересных технических статей:

* [Дизайн и эволюция constexpr в C\+\+](https://pvs-studio.ru/ru/blog/posts/cpp/0909/)
* [Вызов виртуальных функций в конструкторах и деструкторах \(C\+\+\)](https://pvs-studio.ru/ru/blog/posts/cpp/0891/)
* [Почему в С\+\+ массивы нужно удалять через delete\[\]](https://pvs-studio.ru/ru/blog/posts/cpp/0973/)
* [Есть ли жизнь без RTTI: пишем свой dynamic\_cast](https://pvs-studio.ru/ru/blog/posts/cpp/0998/)