Почему шаблон функции SFINAE и шаблон обычной функции имеют разные правила привязки?

У меня была функция проверки, которая использовала ключевое слово 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(){}

Что вызывает эту разницу? Обе функции проверки используют зависимый тип в качестве возвращаемого типа, поэтому я предполагал, что они будут вести себя одинаково.

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

Я бы сказал, IFNDR (плохо сформирован, диагностика не требуется) (разные результаты в разных точках создания экземпляра). requires не является (обычным) шаблоном, его экземпляр не создается (а также требует одного и того же результата в другом месте).

Jarod42 22.07.2024 13:10

Кстати, для вашей проверки требуется конструктивный тип по умолчанию.

Jarod42 22.07.2024 13:11
godbolt.org/z/8Poe4Tsz8
Marek R 22.07.2024 13:12

@Jarod42 Не могли бы вы уточнить, какую часть вы называете IFNDR? Объявлять Foo после проверки? Вы говорите, что requires не является шаблоном, но в моем примере он вызывается шаблоном функции, о чем я и имел в виду. Или вы хотите сказать, что использование requires меняет характеристики шаблона функции? Программа проверки, которой я делюсь здесь, — это всего лишь простой пример, показывающий разницу между поведением SFINAE и require, поэтому не беспокойтесь о том, какие типы она поддерживает — меня больше всего интересует, почему эти две функции ведут себя по-разному.

Joshua Maiche 22.07.2024 14:23

У вас есть несколько POI (точек создания экземпляров) (в static_assert, до и после Foo определения + конец TU (единицы перевода)) одной и той же функции шаблона, что приводит к разным результатам, поэтому это IFNDR.

Jarod42 22.07.2024 14:30

«requires — это не шаблон». Я имел в виду, что requires { Foo(T()); } выглядит как шаблон, но имеет другие (но «похожие») правила. Для вашего варианта использования применяются те же ограничения.

Jarod42 22.07.2024 14:46
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я еще немного покопался и думаю, что у меня есть ответ. По сути, версия шаблона 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() жить в теле шаблона функции.

Другие вопросы по теме