Compile-time Evaluation#
Compile-time variable#
constexpr force the variable to be determined in compile time.
constinit ensures the initial value is determined in compile time, while make it changeable afterwards.
You can only use constinit for global / static / thread-local variables.
It can help to solve static initialization order fiasco.
Compile-time function#
constexpr functions are allowed to not get the value at the compile time.
If you want to force the function to be evaluated at compile time, you need consteval.
These two specifiers can also be added in lambda.
Notice that if all operations in lambda are possible to be in constexpr, constexpr is implied (so explicit specification can be omitted).
Compile-time Branch Selection#
Function overload resolution and specialization#
Template specialization#
template < typename T >
void foo ( T val ) {
std :: println ( "val: {}" , val );
}
template <> // specialization
void foo ( int val ) {
std :: println ( "int val: {}" , val );
}
template void foo ( float val ); // instantiation
cpp
Don’t mistake template specialization from explicit template instantiation .
A specialization must be declared before it’s used , otherwise the behavior is implementation-defined.
A full specialization isn’t a template anymore; thus you cannot define it in header file.
You can either use inline, or only write the specialization prototype.
You can add new specifiers (e.g. inline, constexpr)
Overload resolution#
Overload resolution just tries to find “the most precise one” determined by parameters. The order is:
Perfect match or match with minimal adjustments (i.e. decay, add cvqualifier).
Match with promotion, e.g. short -> int, float -> double.
Match with standard conversions (pre-defined ones), e.g. int -> short.
Match with user-defined conversions.
If there are still more than one candidates, more rules will apply:
More “specialized” ones are preferred, including considering value category;
Non-template ones are preferred than template ones;
For pointers, conversion order is: Derived-to-base > void* > bool.
For std::initializer_list, when using universal initialization, it’s preferred over other ones.
And that’s why std::vector(5, 1) is different from std::vector{5, 1}.
Functors are preferred over surrogate functions (i.e. need conversion to become callable functor).
template < typename T > void Func ( T ); // Func(nullptr) matches this -> Func(nullptr_t)
template < typename T > void Func ( T * );
template <> void Func ( int* ); cpp
Formally, we say template A is more specialized than B if:
Hypothesize that there exist concrete types U1, U2, … to substitute all template parameters in A, if it couldn’t be deduced by B, then we say A isn’t more specialized by B.
Maybe neither template is more specialized than the other, which causes ambiguous call .
Judged when calling.
template < typename T > void Func ( T ); // #1
template < typename T > void Func ( T * ); // #2: more specialized
// #2 Func(U1*) -> Can be derived from #1? true --> #2 is more specialized
// #2 Func(U1) -> Can be derived from #2? false --> #1 is not more specialized cpp
Class template specialization#
Specialized class is a separate class, which can have completely different data member and member functions.
Partial specialization
e.g. template<typename T> class Foo(T*) {};
Not allowed to have default template parameter.
Specialization only determines “whether a type matches it”; it doesn’t determine “what a type is”. (can only determined by the primary template)
Allowed to be defined inside the class.
Selection in code block#
template < typename T >
auto get_value ( T t ) {
if constexpr ( std :: is_pointer_v < T > )
return * t ; // deduces return type to int for T = int*
else
return t ; // deduces return type to int for T = int
} cpp
constexpr bool is_constant_evaluated () noexcept {
if consteval {
return true ;
} else {
return false ;
}
} cpp
Tricky Details#
Name lookup#
Names could be divided into two parts:
Dependent / Non-dependent name: if a name depends on template parameter, then it’s dependent name.
Qualified / Non-qualified name: if a name is specified by ::, ., ->, then it’s qualified. A fully qualified name is like ::a.b.
two-phase lookup:
Non-dependent names are looked up when template is defined.
Dependent names are looked up when template is instantiated.
Reason: template may be specialized afterwards.
this->#
template < typename T >
struct A {
int a ;
};
template < typename T >
struct B : public A < T > {
int func () {
return this -> a ; // return a; -> error
};
}; cpp
typename#
you can use typename when:
The type is a qualified name;
It’s not after keywords class/struct/union/enum;
It’s not Base class appears at inheritance specification and ctor.
And you must use typename when:
Match all rules above;
The type is a dependent name;
It’s not the current instantiation.
Since C++20, many rules are relaxed.
template#
template < typename T >
struct S {
template < typename U >
void foo () {}
};
template < typename T >
void bar () {
S < T > s ;
// s.foo<T>(); // error: < parsed as less than operator
s . template foo < T >(); // OK
} cpp
Concept#
require clause and concept#
requires clause#
concept: declares a named type requirement.
It is OK to put it after the function header.
template < typename T >
requires ( ! std :: is_pointer_v < T > ) && ( sizeof ( T ) >= 8 )
T max ( const T & a , const T & b ) {
// ...
}
// same as
template < typename T >
concept NotPtr = ! std :: is_pointer_v < T > && sizeof ( T ) >= 8 ;
template < typename T >
requires NotPtr < T >
T max ( const T & a , const T & b ) {
// ...
} cpp
requires expression#
Simple requirement
Type requirement: template T::value_type;
Compound requirement: {...} noexcept -> ...
The result type will be fed as the first type parameter directly.
Nested requirement: requires ...;
template < typename T >
concept ComprehensiveContainer = requires ( T container , typename T :: value_type elem ) {
container . push_back ( elem );
typename T :: value_type ;
typename T :: iterator ;
{ container . size () } -> std :: convertible_to < std :: size_t> ;
{ container . clear () } noexcept ;
requires requires {
requires std :: default_initializable <typename T :: value_type > ;
requires std :: equality_comparable <typename T :: iterator > ;
};
}; cpp
template < typename T , typename U >
void insert ( T & cont , const std :: ranges :: range_value_t < T > & val ) {
cont . insert ( val );
}
template < typename T >
void insert ( T & cont , const std :: ranges :: range_value_t < T > & val ) requires
requires { cont . push_back ( val ); } {
cont . push_back ( val );
} cpp
For any type deduction & template parameter, you can add a single constraint by abbr.
Concept can be used as boolean expression.
template < typename T >
concept Hashable = requires ( T a )
{
{ std :: hash < T > {}( a ) } -> std :: convertible_to < std :: size_t> ;
};
template < Hashable T >
void f ( T ) {}
// same as
template < typename T >
requires Hashable < T >
void f ( T ) {}
// same as
template < typename T >
void f ( T ) requires Hashable < T > {}
// same as
void f ( Hashable auto /* parameter-name */ ) {} cpp
code
Template class member functions can be constrained. If the constraints are not met, no compilation error will occur, and the member function will simply be removed.
template < std :: integral T >
struct Foo {
auto test () -> bool requires std :: same_as < T , int> {
return true ;
}
};
Foo <long> foo ; // OK
foo . Test (); // error cpp
Concept subsumption#
We can use concept to do specialization, but specialization has matching order.
Compilers don’t do full logical judgement; instead, it only considers equivalence by concept name.
Non-concepts (including !Concept) will be always considered not related.
template < typename T >
class Foo ;
template < typename T >
requires std :: is_pointer_v < T > && ( sizeof ( T ) >= 8 )
class Foo < T > {};
template < typename T >
requires std :: is_pointer_v < T > && ( sizeof ( T ) >= 4 )
class Foo < T > {};
template class Foo < int* >; // error, ambiguous cpp
Universal Reference and Perfect Forwarding#
Universal Reference#
template < typename T >
void func ( T && t ) {} // universal reference
// lvalue -> T: Type&
// const lvalue -> T: const Type&
// rvalue -> T: Type&& cpp
Requirements:
T&&
The template type is provided by the function directly.
The type is the template parameter itself.
template < typename T >
void func ( const T && t ) {} // not universal reference
template <>
void func ( std :: string && str ) {} // not universal reference
template < typename T >
void func ( typename T :: value_type && t ) {} // not universal reference
template < typename T >
class Foo {
void func ( T && ); // not universal reference
}; cpp
A special form of universal reference is auto&&.
Overload resolution on references#
Universal reference is always second-best choice.
Fallback will be disabled if universal reference is used! i.e. non-const lvalue and const rvalue cannot fallback on const& in copy ctor.
Solution: use concept!
Anyway, pay special attention to universal reference ctor!
For any function with universal reference parameter, it’s usually not a good idea to overload.
That will disable many implicit conversions since universal reference is an exact match.
Perfect Forwarding#
std::forward<T>
<T> is necessary.
Universal reference is always a reference; it cannot be a value type.
void func ( auto&& arg ) {
func_ ( std :: forward < decltype ( arg )>( arg ));
} cpp
Reference Collapsing#
Reference Collapsing Rule:
If all references are rvalue reference, the collapsed reference is rvalue reference.
If there is at least one lvalue reference, the collapsed reference is lvalue reference.
template < typename T >
decltype ( auto ) std :: move ( T && obj ) {
return static_cast< std :: remove_reference_t < T >&&> ( obj );
}
template < typename T >
decltype ( auto ) std :: forward ( std :: remove_reference_t < T >& arg ) {
return static_cast< T &&> ( arg );
} cpp
Pay attention to deduction conflicts.
Solution: use std::remove_reference_t<T> or two template type parameters.
template < typename T >
void insert ( std :: vector < T > & vec , T && elem ) {
vec . push_back ( std :: forward < T >( elem ));
}
int main () {
std :: vector < std :: string > vec ; // T: std::string
std :: string str { "test" }; // T: std::string&
// insert(vec, str); // error
} cpp