Доступ частного члена к замене шаблонов и SFINAE

class A { int a; };

template<typename, typename = void>
class test {};

template<typename T>
class test<T,decltype(T::a)> {};

int main() { test<A> a; }

Приведенный выше код компилируется без ошибок в clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final), но не скомпилируется на g++-5 (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904 и g++-6 (Ubuntu 6.2.0-3ubuntu11~16.04) 6.2.0 20160901 с такими ошибками:

main.cpp: In function ‘int main()’:
main.cpp:9:22: error: ‘int A::a’ is private within this context
 int main() { test<A> a; }
                      ^
main.cpp:1:15: note: declared private here
 class A { int a; };

В обоих случаях я скомпилирован с -std=c++11, но эффект тот же для -std=c++14 и -std=c++1z.

Какой компилятор здесь правильно? Я предположил, что по крайней мере с тех пор, как С++ 11, доступ к закрытым членам во время замены шаблона должен вызвать SFINAE, подразумевая, что clang является правильным, а gcc - нет. Есть ли какое-то дополнительное правило, о котором я не знаю?

Для справки, я думаю о примечании в §14.8.2/8 нынешнего стандартного проекта N4606:

Если подстановка приводит к недопустимому типу или выражению, введите вычет заканчивается. Недопустимым типом или выражением является тот, который был бы плохо сформированный, с требуемой диагностикой, если он написан с использованием замещенные аргументы. [Примечание: если диагностика не требуется, программа по-прежнему плохо сформирована. Проверка доступа выполняется как часть процесс замещения. - конечная нота]

Использование функции-члена и указателя функции-члена вместо этого принимается обоими компиляторами:

class A { void a() {}; };

template<typename, typename = void>
class test {};

template<typename T>
class test<T,decltype(&T::a)> {};

int main() { test<A> a; }

Ответы

Ответ 1

Это очень интересно! Я думаю, что это ошибка компилятора g++ и Я думаю, что это то, что происходит. Я пробовал несколько изменений вашего кода с g++ 4.9.3 и clang 3.7.0.

Несмотря на то, что существуют несколько разных правил для функции создания экземпляров класса vs, я считаю, что это общие шаги для создания экземпляра шаблона:

  • Компилятор читает исходный файл с определениями шаблонов.
  • Поиск имени (может вызвать ADL): это процедура, с помощью которой имя, когда встречается в программе, связано с объявлением который представил его. (http://en.cppreference.com/w/cpp/language/lookup)
  • Спецификация/вывод аргумента шаблона: для того, чтобы создать экземпляр шаблона функции, каждый аргумент шаблона должен быть но не каждый аргумент шаблона должен быть указан. когда возможно, компилятор выведет отсутствующие аргументы шаблона из аргументов функции. (http://en.cppreference.com/w/cpp/language/template_argument_deduction)
  • Подстановка шаблона (может вызвать SFINAE): каждое использование параметр шаблона в списке параметров функции заменяется на соответствующие аргументы шаблона. Ошибка отказа (т.е. невозможность замены параметров шаблона с помощью выведенного или предоставленного аргументы шаблона) шаблона функции удаляет функцию шаблон из набора перегрузки. (http://en.cppreference.com/w/cpp/language/function_template#Template_argument_substitution)
  • Формирование набора перегрузки: перед началом разрешения перегрузки функции, выбранные при поиске имени и выводе аргумента шаблона объединяются для формирования набора функций-кандидатов. (http://en.cppreference.com/w/cpp/language/overload_resolution#Details)
  • Разрешение перегрузки: в целом, функция-кандидат, чья параметры, наиболее близкие к аргументам, являются называется. (http://en.cppreference.com/w/cpp/language/overload_resolution)
  • Создание шаблона: аргументы шаблона должны быть определены так что компилятор может генерировать действительную функцию (или класс, из шаблон класса). (http://en.cppreference.com/w/cpp/language/function_template#Function_template_instantiation)
  • Компилятор генерирует код.

Я буду держать эти маркеры в качестве рекомендаций и ссылок для дальнейшего использования. Кроме того, я буду ссылаться на оценку шаблона с шагов 1-6. Если вы нашли что-то не так в приведенном выше списке , пожалуйста, пожалуйста, не стесняйтесь изменять его или комментировать, чтобы я мог внести изменения.

В следующем примере:

class A {};

template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };

template<typename T>
struct test<T, decltype(T::a)>
{ test(){std::cout<< "Using T::a" <<std::endl;} };

int main()
{ test<A> a; }

Вывод обоих компиляторов:

Using None

Этот пример отлично компилируется как в g++, так и в clang, потому что, когда компилятор завершит процесс оценки всех шаблонов, он будет выбирать только экземпляр первого шаблона для наилучшего соответствия аргументам шаблона, используемым для создания объекта в main(). Кроме того, процесс замены шаблона выходит из строя, когда компилятор не выводит T:: a (SFINAE). Кроме того, из-за несоответствия аргументов специализация не будет включена в набор перегрузки и не будет создана.

Следует добавить второй аргумент шаблона, например:

test<A , decltype(A::a)> a;

Код не будет компилироваться, и оба компилятора будут жаловаться на:

error: no member named 'a' in 'A'

В следующем примере, однако, все начинает становиться странным:

class A { int a; };

template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };

template<typename T>
struct test<T, decltype(T::a)>
{ test(){std::cout<< "Using T::a" <<std::endl;} };

int main()
{ test<A> a; }

Выход из clang:

Using None

Вывод из g++:

error: ‘int A::a’ is private

Начнем с того, что это было бы хорошим предупреждением. Но почему ошибка? Шаблон даже не будет создан. Учитывая предыдущий пример и тот факт, что указатели-к-членам являются постоянными значениями, известными во время компиляции, кажется, что когда clang завершает этап оценки шаблона, когда SFINAE возникает при замене шаблона, он точно создает первый шаблон и игнорирует специализация. Но когда g++ проходит процесс подстановки и ищет переменную T:: a, он видит, что это частный член, и вместо того, чтобы говорить SFINAE, он запрашивает ошибку выше. Я думаю, что это ошибка, учитывая этот отчет об ошибке: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61806

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

class A{ void a() {}; };

template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };

template<typename T>
struct test<T,decltype(&T::a)>
{ test(){std::cout<< "Using A::a" <<std::endl;} };

int main()
{ test<A> a; }

Вывод обоих компиляторов:

Using None

Если предыдущее объяснение истинно, то почему g++ не вызывает ошибку при использовании функции private member? Опять же, это только предположение, основанное на результатах, но я думаю, что этот бит фактически работает так, как должен. Короче говоря, SFINAE запускается, специализация отбрасывается из набора перегрузки, и создается только первый шаблон. Может быть, там больше, чем кажется на первый взгляд, но если мы явно укажем второй аргумент шаблона, оба компилятора будут сообщать ту же ошибку.

int main()
{ test<A , decltype(&A::a)> b; }

Вывод обоих компиляторов:

error: ‘void A::a()’ is private

В любом случае, это последний код, который я использовал. Чтобы продемонстрировать результаты, я сделал класс общедоступным. В качестве интересного события я добавил nullptr, чтобы напрямую указать на функцию-член. Тип из decltype (((T *) nullptr) → f()) является void, а из приведенного ниже примера a и c оба вызываются специализацией а не первый шаблон. Причина в том, что второй шаблон более специализирован, чем первый, и, следовательно, лучше всего подходит для обоих из них (убить двух зайцев одним выстрелом) (Формальные правила оформления шаблонов: fooobar.com/info/386542/...). Тип decltype (& T:: f) - M4GolfFvvE (возможный перевод: Men 4 Golf Fear очень злобный лоси), который благодаря boost:: typeindex:: type_id_with_cvr, он помечен в void (Golf:: *)().

#include <iostream>
#include <boost/type_index.hpp>

class Golf
{
    public:
        int v;

        void f()
        {};
};


template<typename T>
using var_t = decltype(T::v);

template<typename T>
using func_t = decltype(&T::f);
//using func_t = decltype(((T*)nullptr)->f()); //void


template<typename, typename = void>
struct test
{
    test(){std::cout<< "Using None" <<std::endl;}
};

template<typename T>
struct test<T,var_t<T> >
{
    test(){std::cout<< "Using Golf::v" <<std::endl;}
};

template<typename T>
struct test<T,func_t<T> >
{
    test(){std::cout<< "Using Golf::f" <<std::endl;}
};


int main()
{
    test<Golf> a;
    test<Golf,var_t<Golf> > b;
    test<Golf,func_t<Golf> > c;

    using boost::typeindex::type_id_with_cvr;
    std::cout<< typeid(func_t<Golf>).name() << " -> " << type_id_with_cvr<func_t<Golf>>().pretty_name() <<std::endl;
}

Вывод обоих компиляторов (func_t = decltype (& T:: f)):

Using None
Using Golf::v
Using Golf::f
M4GolfFvvE -> void (Golf::*)()

Вывод обоих компиляторов (func_t = decltype (((T *) nullptr) → f())):

Using Golf::f
Using Golf::v
Using Golf::f
v -> void