Какой смысл выражения ограничения на не шаблонную функцию?

[temp.constr.decl] говорит, что мы можем ограничить шаблон или функцию с помощью выражения ограничения.

Заявители [dcl.decl] говорят нам, что для функций мы можем добавить необязательный конечный пункт require, чтобы ограничить его, и стандартный черновик n4820 даже приводит эти (казалось бы, бессмысленные) примеры:

void f1(int a) requires true;
auto f2(int a) -> bool requires true;

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

Ответ 1

Одним из основных пунктов ограничения не шаблонных функций является возможность записи ограничений для не шаблонных членов шаблонных классов. Например, у вас может быть такой тип:

template<typename T>
class value
{
public:
  value(const T& t);
  value(T&& t);

private:
  T t_;
};

Теперь вы хотите, чтобы value было копируемым/перемещаемым из T Но на самом деле вы хотите, чтобы он был копируемым/перемещаемым из T только в той степени, в которой сам T является копируемым/перемещаемым. Итак, как вы это делаете?

Предварительные ограничения, вам нужно написать кучу хакеров метапрограммирования. Возможно, вы создаете эти шаблоны конструкторов, которые требуют, чтобы данный тип U был таким же, как и T, в дополнение к требованию копирования/перемещения. Или вам может потребоваться написать базовый класс, от которого вы наследуете, который имеет различные специализации, основанные на копировании/перемещаемости T

Пост-ограничения, вы делаете это:

template<typename T>
class value
{
public:
  value(const T& t) requires is_copy_constructible_v<T> : t_(t) {}
  value(T&& t) requires is_move_constructible_v<T> : t_(std::move(t)) {}

private:
  T t_;
};

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

Это особенно важно для функций, которые не могут быть шаблонами. Чтобы конструктор считался конструктором копирования или перемещения, он не может быть шаблоном. То же самое касается операторов копирования/перемещения. Но такие вещи могут иметь ограничения.

Ответ 2

В качестве концепции рассмотрим следующий пример

#include <iostream>

void f( long x ) requires ( sizeof( long ) == sizeof( int ) )
{
    std::cout << "Bye " << x << '\n';
}

void f( long long x ) requires ( sizeof( long ) == sizeof( long long ) )
{
    std::cout << "Hello " << x << '\n';
}

int main() 
{
    f( 0l );
}

Если sizeof( long ) == sizeof( long long ) вывод программы будет

Hello 0

Иначе

Bye 0

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

Вот показательная программа.

#include <iostream>
#include <stdexcept>

unsigned long factorial( unsigned long n ) noexcept( false ) 
    requires ( sizeof( unsigned long ) == sizeof( unsigned int ) )
{
    const unsigned long MAX_STEPS = 12;

    if ( MAX_STEPS < n ) throw std::out_of_range( "Too big value." );

    unsigned long f = 1;

    for ( unsigned long i = 1; i < n; i++ ) f *= ( i + 1 );

    return f;
}

unsigned long long factorial( unsigned long long n ) noexcept( false ) 
    requires ( sizeof( unsigned long ) == sizeof( unsigned long long ) )
{
    const unsigned long long MAX_STEPS = 20;

    if ( MAX_STEPS < n ) throw std::out_of_range( "Too big value." );

    unsigned long f = 1;

    for ( unsigned long long i = 1; i < n; i++ ) f *= ( i + 1 );

    return f;
}

int main() 
{
    unsigned long n = 20;

    try
    {
        std::cout << factorial( n ) << '\n';
    }
    catch ( const std::out_of_range &ex )
    {
        std::cout << ex.what() << '\n';
    }
}

Его вывод может быть либо

2432902008176640000

или же

Too big value.