Каков идиоматичный способ иметь функцию-член шаблонного класса с объявлением, действительным только если аргумент шаблона соответствует какому-либо концепту?

Вопрос или проблема

Функция языка 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.

Оцените материал
Добавить комментарий

Капча загружается...