Будет ли класс, объявленный в инициализаторе члена конструктора другого класса, видимым за его пределами?

Рассмотрим следующий фрагмент кода:

struct Foo { 
    void* p; 
    Foo() : p{(class Bar*)0} {}
};

Bar* bar;

Последние версии GCC (8.2) и Clang (7.0.0) не могут его скомпилировать. То же самое делает ICC (19.0.1).
Однако MSVC (v19.16) компилирует его чисто.

Ошибка от GCC: error: 'Bar' does not name a type; did you mean 'char'? error: 'Bar' does not name a type; did you mean 'char'?
Clang и ICC выдают похожие сообщения.

Просмотрщик соответствия для всех четырех компиляторов в Godbolt.

Итак, какой из компиляторов верен в соответствии со стандартом?

Ответ 1

  • [basic.lookup.elab]... Если описатель типа введен с помощью ключа класса, и этот поиск не находит ранее объявленное имя типа... указатель разработанного типа является объявлением, которое вводит имя класса, как описано в [basic.scope.pdecl]

  • [basic.scope.pdecl] - для подробного спецификатора типа формы

    идентификатор класса

    если разработанный спецификатор типа используется в выражении decl-specier-seq или параметр-объявления функции, определенной в области пространства имен [не применяется из-за области действия],... в противном случае, за исключением объявления друга, идентификатор объявлен в наименьшем пространстве имен или области блока, которая содержит объявление.

Теперь немного хитрый. Список инициализаторов членов "содержится" в области конструктора или нет? Если нет, то наименьшая область блока или пространства имен - это глобальное пространство имен, и программа будет правильно сформирована. Если да, то наименьшая область действия - это область блока конструктора, и поэтому класс не будет объявлен в глобальной области.

Насколько я могу судить, нет правила, гласящего, что mem-init-list "содержится в области видимости конструктора". Это находится вне фигурных скобок, которые ограничивают область. Таким образом, программа хорошо сформирована.

Mem-init-list является частью тела конструктора [dcl.fct.def.general], но это тело не является ни областью блока, ни областью пространства имен, поэтому оно не относится к правилу в [basic.scope.pdecl],

Ответ 2

Это о правилах декларации и правилах области видимости; (разработанный спецификатор типа может также объявить имя класса в выражении).

scope.declarative/3: имена, объявленные объявлением, вводятся в область, в которой происходит объявление, за исключением того, что присутствие спецификатора друга, определенные использования разработанного спецификатора типа ([dcl.type.elab]) и using-директивы ([namespace.udir]) изменяют это общее поведение.

у конструктора есть область (как упоминается в ответе eeroika), аналогично всему, что там заявлено. Вот почему это должно быть в силе

struct Foo { 
    void* p; 
    Foo() : p{(class Bar*)0} {
        Bar* bar;
    }
};

https://godbolt.org/z/m3Tdle

Но нет:

struct Foo { 
    void* p; 
    Foo() : p{(class Bar*)0} {
        //Scope of Bar only exists here
    }
};

Bar* bar;

РЕДАКТИРОВАТЬ: Подробнее об этом:

Так называемое "предварительное объявление" в C++ технически является одной из множества "форм" elaborated-type-specifier; И только две формы могут вводить имя. (Акцент мой)

Точка объявления класса, впервые объявленного в подробном спецификаторе типа, следующая:

  • (7.1) для декларации формы

    ключ-атрибут-спецификатор-seq opt- идентификатор; //<-- Note the semi colon

    идентификатор объявляется как имя класса в области, содержащей объявление, в противном случае

  • (7.2) для уточненного спецификатора типа формы

    идентификатор класса

    если разработанный спецификатор типа используется в выражении decl-specier-seq или параметр-объявление функции, определенной в области пространства имен, идентификатор объявляется как имя класса в пространстве имен, которое содержит объявление; в противном случае, за исключением объявления друга, идентификатор объявляется в наименьшем пространстве имен или области блока, содержащей объявление. [Примечание: эти правила также применяются в шаблонах. - примечание конца] [Примечание: Другие формы разработанного спецификатора типа не объявляют новое имя, и поэтому должны ссылаться на существующее имя типа. Смотрите [basic.lookup.elab] и [dcl.type.elab]. —- конец примечания]


Вот еще один интересный момент об этом;

dcl.type.elab/1 Атрибут-спецификатор-seq не должен появляться в подробном спецификаторе типа, если только последний не является единственной составляющей объявления...

Вот почему это (ниже) действительно, смотрите его здесь https://godbolt.org/z/IkmvGn;

void foo(){
    void* n = (class Boo*)(0);
    Boo* b;
}

class [[deprecated("WTF")]] Mew;

Но это (ниже) неверно 1; Смотрите его здесь https://godbolt.org/z/8X1QKq;

void foo(){
    void* n = (class [[deprecated("WTF")]] Boo*)(0);
}

class [[deprecated("WTF")]] Mew;
  • 1 странно, GCC принимает это, но дает это предупреждение:

    attributes ignored on elaborated-type-specifier that is not a forward declaration [-Wattributes]
    

Чтобы увидеть все другие "формы" elaborated-type-specifier смотрите dcl.type.elab


Редактировать 2

Чтобы еще раз проверить мое понимание, я спросил еще, и @Simon Brand заставил меня осознать некоторые крайние случаи, на что частично намекает basic.scope/Declarative-4.note-2. Но в основном по второй цитате в этом ответе basic.scope/pdecl-7.2

Сложный спецификатор типа в пределах области параметров функции будет просачивать свое имя класса во вмещающее пространство имен (примечание: это не область действия класса).

Это действительно https://godbolt.org/z/Fx5B83:

struct Foo { 
    static void foo(void* = (class Bat*)0); //Leaks it past class-scope
};

void moo(){
    Bat* m;
}

Даже если вы прячете его во вложенные классы https://godbolt.org/z/40Raup:

struct Foo { 
    class Moo{
        class Mew{ 
            void foo(void* = (class Bat*)0); //
        };
    };
};

void moo(){
    Bat* m;
}

... Однако имя перестает вытекать в ближайшем namespace https://godbolt.org/z/YDljDo.

namespace zoo {
    struct Foo { 
        class Moo{
            class Mew{ 
                void foo(void* = (class Bat*)0);
            };
        };
    };
}
void moo(){
    zoo::Bat* m;
}

В заключение,

mem-init-list является составным оператором и не имеет функции-параметра-области видимости; Итак, если вы хотите добиться утечки, сделайте объявление в function-parameter-scope. Смотрите https://godbolt.org/z/CqejYS

struct Foo { 
    void* p; 
    Foo(void* = (class Zoo*)(0)) 
        : p{(class Bar*)0} {
        Bar* bar;
    }
};

Zoo* m;     //Zoo* leaked
//Bar* n    //Bar* did not leak