Вопрос или проблема
Функция языка C++20 Concepts позволяет ограничивать функции-члены шаблонных классов. Однако эти ограничения применяются только к телу функций-членов, а не к объявлению – объявление все еще должно быть допустимым.
Я сталкиваюсь с ситуацией, когда объявление может быть семантически недопустимым, но я хотел бы избежать ошибки компиляции, пока пользователь не попытается вызвать эту функцию-член.
Например:
#include <vector>
template <typename T>
struct A {
// как переписать это объявление, чтобы оно работало?
typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
return x * 10;
}
};
int main(){
A<std::vector<int>> a; // компилируется
A<int> b; // не компилируется, но мы хотим, чтобы это компилировалось, пока пользователь не вызовет `b.f`
struct B { using value_type = void; };
A<B> c; // не компилируется, но мы хотим, чтобы это компилировалось, пока пользователь не вызовет `c.f`
}
Ошибка компиляции:
<source>: In instantiation of 'struct A<int>':
<source>:12:12: required from here
12 | A<int> b; // не компилируется
| ^
<source>:5:28: error: 'int' is not a class, struct, or union type
5 | typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
| ^
<source>: In instantiation of 'struct A<main()::B>':
<source>:14:10: required from here
14 | A<B> c; // не компилируется
| ^
<source>:5:28: error: invalid parameter type 'main()::B::value_type' {aka 'void'}
5 | typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
| ^
<source>:5:28: error: in declaration 'typename T::value_type A<T>::f(typename T::value_type) requires integral<typename T::value_type>'
Каков идиоматический способ переписать объявление функции-члена f
, чтобы код компилировался?
Я не уверен по поводу идиоматического, но есть трюк, который вы можете использовать — иметь идентичный аргумент шаблона, который не будет заменен до более позднего времени:
template<typename U = T>
requires std::integral<typename T::value_type>
typename U::value_type f(typename U::value_type x) {
return x * 10;
}
Ожидается, что этот аргумент шаблона никогда не будет предоставлен явно. Он всегда должен быть таким, что U
– тот же тип, что и T
, но поскольку это возможно изменить, замены не произойдет на этапе T
, поэтому жесткие ошибки избегаются.
Можно утверждать, что использование T
против U
в requires
имеет значение, но это не должно влиять на поведение, когда f
используется правильно и они одного типа. requires
обработает сбои замены, не выполняя требование.
Другой способ – поместить специфический код в базовый класс с специализацией:
template <typename T>
struct A_Base {};
template <typename T>
requires std::integral<typename T::value_type>
struct A_Base<T> {
T::value_type f(T::value_type x) { return x * 10; }
};
template <typename T>
struct A : A_Base<T>
{
};
Ответ или решение
Как реализовать методы шаблонного класса с ограничениями, зависящими от концептов в C++20
В C++20 в язык была внедрена поддержка концептов, что позволяет наложить ограничения на параметры шаблонов и их методы. Однако, как вы уже заметили, ограничения действуют только на тело метода, а не на его объявление, что может привести к ошибкам компиляции, если тип параметра шаблона не соответствует требованиям.
Проблема
Предположим, у нас есть следующий код:
#include <vector>
template <typename T>
struct A {
typename T::value_type f(typename T::value_type x) requires std::integral<typename T::value_type> {
return x * 10;
}
};
int main(){
A<std::vector<int>> a; // компилируется
A<int> b; // не компилируется, но мы хотим, чтобы он компилировался
struct B { using value_type = void; };
A<B> c; // не компилируется, но мы хотим, чтобы он компилировался
}
В этом примере, когда мы пытаемся создать экземпляр A<int>
, компилятор выдает ошибку, так как int
не относится к классу, у которого есть value_type
. Мы хотим избежать таких ошибок на этапе компиляции, пока метод не будет вызван.
Решения
1. Использование дополнительного шаблона
Один из приемов заключается в использовании дополнительного шаблонного параметра, который будет иметь значение по умолчанию. Это позволит нам отделить семантическую проверку от декларации метода:
template <typename T>
struct A {
template <typename U = T>
typename U::value_type f(typename U::value_type x) requires std::integral<typename U::value_type> {
return x * 10;
}
};
Здесь U
— временный параметр, который будет зависеть от T
, но при этом позволяет избежать ошибок компиляции на этапе объявления метода.
2. Использование базового класса с частичными специализациями
Другой подход — создание базового класса с использованием частичной специализации для реализации нужного функционала:
template <typename T>
struct A_Base {};
template <typename T>
requires std::integral<typename T::value_type>
struct A_Base<T> {
typename T::value_type f(typename T::value_type x) { return x * 10; }
};
template <typename T>
struct A : A_Base<T>
{
};
В данном случае мы используем частичные специализации, чтобы определять, как будет работать метод f
, без вызова ошибок компиляции для тех типов, которые не поддерживают требуемые операции.
Заключение
Эти подходы позволяют избежать ошибок компиляции, связанных с признаком, когда вы используете его в качестве параметра шаблона. Используя концепты и более гибкие шаблонизации, вы можете реализовать более безопасный и гибкий код, который соответствует новейшим стандартам C++. Выбор подхода зависит от ваших предпочтений и структуры вашего кода, но оба метода являются "идиоматическими" для работы с шаблонами и концептами в C++20.