What is the point of a constraint expression on a non-templated function?
Just as a concept consider the following example
#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 );
}
If sizeof( long ) == sizeof( long long )
then the program output will be
Hello 0
Otherwise
Bye 0
For example you can use such an approach in a function that calculates the factorial to restrict the number of a loop iterations or to throw an exception.
Here is a demonstrative program.
#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';
}
}
Its output might be either
2432902008176640000
or
Too big value.
One of the main points of constraining non-template functions is to be able to write constraints to non-template members of template classes. For example, you might have some type like this:
template<typename T>
class value
{
public:
value(const T& t);
value(T&& t);
private:
T t_;
};
Now, you want value
to be copyable/moveable from T
. But really, you want it to be copyable/moveable from T
only as far as T
itself is copyable/moveable. So, how do you do it?
Pre-constraints, you would need to write a bunch of meta-programming hackery. Maybe you make these constructors templates, which require that the given type U
is the same as T
, in addition to the copy/move requirement. Or you might have to write a base class that you inherit from, which has different specializations based on the copy/moveability of T
.
Post-constraints, you do this:
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_;
};
No hackery. No applying templates to functions that don't need to be templates. It just works, and it's easy for the user to understand what is going on.
This is especially important for functions which cannot be templates. In order for a constructor to be considered a copy or move constructor, it cannot be a template. Same goes for copy/move assignment operators. But such things can have constraints.