

Modern C++ Basics - Template Basics and Move Semantics
typename T: Where Compilers Cry and Coders Become Wizards
views
| comments
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.
- You can only use
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
)
- You can either use
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 fromstd::vector{5, 1}
.
- And that’s why
- 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.
- Hypothesize that there exist concrete types
- 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
cppClass 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.
- e.g.
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
}
cppconstexpr bool is_constant_evaluated() noexcept {
if consteval {
return true;
} else {
return false;
}
}
cppTricky 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
};
};
cpptypename
#
- 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
}
cppConcept#
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) {
// ...
}
cpprequires 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>;
};
};
cpptemplate <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- 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
cppConcept 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.
- Non-concepts (including
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
cppUniversal 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));
}
cppReference 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.
- Solution: use
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