У меня была функция проверки, которая использовала ключевое слово requires
, чтобы определить, определена ли целевая функция или нет. Я хотел, чтобы это работало с C++17, поэтому я переключил проверку с использования requires
на использование выражения SFINAE.
Именно тогда я был удивлен, обнаружив, что если я вызову обе программы проверки без определения целевой функции, затем определю целевую функцию и проверю снова, программа проверки requires
будет повторно использовать старый результат, в то время как программа проверки SFINAE обновится, чтобы правильно показать, что целевая функция определена. .
// Compiles on Clang, GCC, and MSVC.
#include <type_traits>
struct MyStruct {};
constexpr bool Checker_SFINAE(...)
{
return false;
}
template <typename T>
constexpr auto Checker_SFINAE(T) -> decltype(Foo(T()), bool())
{
return true;
}
template <typename T>
constexpr auto Checker_requires(T)
{
return std::bool_constant<requires { Foo(T()); }>();
}
static_assert(!Checker_SFINAE(MyStruct()));
static_assert(!Checker_requires(MyStruct()));
void Foo(MyStruct) {}
static_assert(Checker_SFINAE(MyStruct()));
// Will fail is the previous Checker_requires is removed
static_assert(!Checker_requires(MyStruct()));
int main(){}
Что вызывает эту разницу? Обе функции проверки используют зависимый тип в качестве возвращаемого типа, поэтому я предполагал, что они будут вести себя одинаково.
С практической точки зрения, что заставляет шаблон функции интерпретироваться по-разному в разных экземплярах, даже если все параметры шаблона остаются одинаковыми?
Кстати, для вашей проверки требуется конструктивный тип по умолчанию.
@Jarod42 Не могли бы вы уточнить, какую часть вы называете IFNDR? Объявлять Foo
после проверки? Вы говорите, что requires
не является шаблоном, но в моем примере он вызывается шаблоном функции, о чем я и имел в виду. Или вы хотите сказать, что использование requires
меняет характеристики шаблона функции? Программа проверки, которой я делюсь здесь, — это всего лишь простой пример, показывающий разницу между поведением SFINAE и require, поэтому не беспокойтесь о том, какие типы она поддерживает — меня больше всего интересует, почему эти две функции ведут себя по-разному.
У вас есть несколько POI (точек создания экземпляров) (в static_assert
, до и после Foo
определения + конец TU (единицы перевода)) одной и той же функции шаблона, что приводит к разным результатам, поэтому это IFNDR.
«requires
— это не шаблон». Я имел в виду, что requires { Foo(T()); }
выглядит как шаблон, но имеет другие (но «похожие») правила. Для вашего варианта использования применяются те же ограничения.
Я еще немного покопался и думаю, что у меня есть ответ. По сути, версия шаблона Checker_SFINAE()
не создается до тех пор, пока не будет объявлен Foo()
.
Начнем с основ. При вызове перегруженной функции в этот момент будет вызвана та перегрузка функции, которая работает лучше всего. Если позже мы добавим еще одну перегрузку и снова вызовем функцию, эта новая перегрузка будет включена в список кандидатов.
struct MyStruct {};
constexpr bool HasOverload(...)
{
return false;
}
static_assert(!HasOverload(MyStruct{}));
constexpr bool HasOverload(MyStruct)
{
return true;
}
static_assert(HasOverload(MyStruct{}));
Теперь вернемся к примеру вопроса, слегка измененному:
#include <type_traits>
struct MyStruct {};
constexpr bool Checker(...)
{
return false;
}
template <typename T>
constexpr std::enable_if_t<requires { Foo(T()); }, bool> Checker(T)
{
return true;
}
static_assert(!Checker(MyStruct()));
void Foo(MyStruct) {}
static_assert(Checker(MyStruct()));
При первом вызове Checker()
здесь подпись шаблона функции не сможет скомпилироваться, если бы она была создана. Благодаря SFINAE компилятор избегает создания экземпляра шаблона. Единственным нашим Checker()
на данный момент остается Checker(...)
.
Когда Checker()
вызывается во второй раз, после объявления Foo()
, компилятор снова просматривает кандидатов. На этот раз сигнатуру шаблона функции можно создать без ошибок компиляции, поэтому теперь создается экземпляр шаблона. Теперь этот экземпляр шаблона является лучшим кандидатом, поэтому он используется, и Check()
возвращает true.
Важно отличать выбор лучшего кандидата на перегрузку, который происходит каждый раз при вызове функции, от создания экземпляра шаблона функции, который происходит только один раз. Как только произойдет создание экземпляра шаблона, пути назад уже не будет; эта новая функция теперь является кандидатом. Создание экземпляра шаблона функции с теми же параметрами шаблона приведет к созданию той же функции, поскольку она уже была создана.
Например, предположим, что нам нужно инвертировать Checker, поэтому базовый случай возвращает true, а перегрузка использует SFINAE для проверки отсутствия Foo()
:
#include <type_traits>
struct MyStruct {};
constexpr bool Checker(...)
{
return true;
}
template <typename T>
constexpr std::enable_if_t<!requires { Foo(T()); }, bool> Checker(T)
{
return false;
}
static_assert(!Checker(MyStruct()));
void Foo(MyStruct) {}
// Note how this hasn't changed.
static_assert(!Checker(MyStruct()));
После создания экземпляра шаблона джина уже невозможно загнать обратно в бутылку. Теперь он существует и будет рассматриваться как кандидат на Foo()
. Это означает, что если Checker()
когда-либо вернет false с типом, он всегда будет возвращать false с этим типом.
Подводя итог, можно сказать, что создание экземпляра шаблона и разрешение перегрузки функции — это две разные вещи. Создание экземпляра происходит один раз; разрешение перегрузки происходит при каждом вызове функции. Checker_SFINAE()
вводит новых кандидатов на перегрузку после объявления Foo()
, поэтому он меняет свое возвращаемое значение. Checker_requires()
— это просто шаблон функции, поэтому после создания его экземпляра он фиксируется на этом значении. Ключевое слово requires
было отвлекающим маневром — оно не имело никакого влияния на поведение функции, за исключением того, что позволяло проверке Foo()
жить в теле шаблона функции.
Я бы сказал, IFNDR (плохо сформирован, диагностика не требуется) (разные результаты в разных точках создания экземпляра).
requires
не является (обычным) шаблоном, его экземпляр не создается (а также требует одного и того же результата в другом месте).