Компиляторы С++ расходятся в поведении инкапсуляции - какой из них прав?

Компиляторы (clang-5.0.0, GCC-7.3, ICC-18 и MSVC-19) расходятся по доступности членов A ниже.

class A {

    template <class> static constexpr int f() { return 0; }

    template <int> struct B {};

    template <class T> using C = B<f<T>()>;

};

Действительно, рассмотрим следующие способы использования:

template <class T> using D = A::C<T>;

int main() {
                        //    | clang | gcc | icc | msvc
    (void) A::f<int>(); // 1: | f     | f   | f   | f, (C)
    (void) A::B<0>{};   // 2: | B     |     | B   | B, (C)
    (void) A::C<int>{}; // 3: | C,  f |     | C   | C
    (void) D<int>{};    // 4: | f     |     | C   | C
}

В таблице справа показано, какие члены должны быть опубликованы каждым компилятором для принятия кода (при компиляции для С++ 14).

ИМХО, ICC и MSVC (игнорируя (C) записи) выглядят корректно. За исключением первой строки, GCC, похоже, полностью игнорирует доступность.

Я не согласен с clang, когда требуется, чтобы f был общедоступным для создания экземпляров A::C<int> и D<int>. Как и ICC и MSVC, я думаю, что C и только C должны быть публичными. Это правда, что C использует f но разве это не деталь реализации? Обратите внимание, что C также использует B Если clang были верны, то почему он не требует, чтобы B был публичным?

Наконец, давайте рассмотрим записи (C). MSVC требует, чтобы C был общедоступным, когда он впервые встречает определение D, то есть MSVC жалуется на то, что C является частным.

Мои вопросы:

  1. Прав ли я (и ICC) в моем анализе? Иначе какой другой компилятор верен и почему?
  2. Является ли проблема MSVC еще одним воплощением ошибки двухфазной реализации в msvc?

Обновление: Что касается GCC, похоже, это ошибка, о которой сообщалось в комментарии 8, здесь.

Ответы

Ответ 1

Вопросы A::f<int>() и A::B<0> являются прямыми для ответа. f и B являются частными, и у них нет других интересных зависимостей. Доступ к ним должен быть плохо сформирован. gcc, как правило, очень правдоподобно в отношении контроля доступа в шаблонах, для всех видов ситуаций существует метабог (я думаю, что все они имеют такую форму, что gcc разрешает доступ, когда это не должно, а не запрещает доступ, когда это необходимо).

Интереснее вопрос A::C<int>. Это шаблон псевдонима, но в каком контексте мы действительно просматриваем псевдоним? Является ли он внутри A (в этом случае, если C будет достаточным) или он будет использоваться в контексте, в котором он используется (в этом случае все f, B и C должны быть доступны). Этот вопрос - именно CWG 1554, который все еще активен:

Взаимодействие шаблонов псевдонимов и контроля доступа не ясно из текущей редакции 17.6.7 [temp.alias]. Например:

template <class T> using foo = typename T::foo;

class B {
  typedef int foo;
  friend struct C;
};

struct C {
  foo<B> f;    // Well-formed?
};

Является ли замена B::foo для foo<B> выполненной в контексте подкласса класса C, что делает корректную формулу ссылки, или это доступ определен независимо от контекста, в котором появляется спецификация шаблона псевдонимов?

Если ответ на этот вопрос заключается в том, что доступ определяется независимо от контекста, необходимо следить за тем, чтобы отказ доступа по-прежнему считался "в непосредственном контексте типа функции" (17.9.2 [temp.educt ], пункт 8), так что это приведет к отказу от вычета, а не к жесткой ошибке.

Хотя проблема все еще остается открытой, направление выглядит следующим образом:

Консенсус CWG заключался в том, что создание экземпляров (поиск и доступ) для шаблонов псевдонимов должно быть таким же, как и для других шаблонов, в контексте определения, а не в контексте, в котором они используются. Однако они все равно должны быть расширены.

Иными словами, только C необходимо обнародовать, а f и B могут оставаться закрытыми. Так интерпретируют ICC и MSVC. У Clang есть ошибка, которая позволяет шаблонам псевдонимов обходить доступ (15914), поэтому clang требует, чтобы f был доступен, но не B Но в противном случае clang, похоже, расширяет псевдоним в точке использования, а не в точке определения.

Вопрос D<int> должен просто следовать A::C точно, никаких проблем с CWG 1554 здесь нет. Clang - единственный компилятор, который может иметь различное поведение между A::C и D, снова из-за ошибки 15914.


Подводя итог, вопрос A::C - проблема открытого ядра, но ICC реализует намеченное значение языка здесь. У других компиляторов есть проблемы с проверкой доступа и шаблонами.