元编程 (metaprogramming):以类或函数等程序实体 (entity) 为运算对象的编程方式。
在 C++ 中,元编程主要通过以下语言机制来实现:
template
生成类或函数。constexpr
完成一些编译期常量的计算。元编程这种编程技巧 (technique)(强调编译期计算)为实现泛型编程这种编程范式 (paradigm)(强调算法和数据类型的抽象)提供了技术支持。
⚠️ 过度使用元编程会使得代码可读性差、编译时间长、测试难度大。
类型函数不是普通函数,而是借助于类模板实现的(以类型或编译期常量为运算对象的)编译期运算机制。
std::remove_reference
定义在 <type_traits>
中的类模板 std::remove_reference
用于移除 (remove) 类型实参的引用 (reference):
namespace std{
template <class T> struct remove_reference { typedef T type; };
template <class T> struct remove_reference<T&> { typedef T type; };
template <class T> struct remove_reference<T&&> { typedef T type; };
} // namespace std
使用时,它以类型实参为输入,以类型成员为输出。 在 C++14 以前,类模板的类型成员必须通过 typename
来访问,这使得代码变得冗长:
#include <type_traits>
int main() {
typename std::remove_reference<int >::type x{0}; // 等价于 int x{0};
typename std::remove_reference<int& >::type y{0}; // 等价于 int y{0};
typename std::remove_reference<int&&>::type z{0}; // 等价于 int z{0};
}
自 C++14 起,标准库为它提供了以 _t
为后缀的别名 (alias):
namespace std{
template <class T>
using remove_reference_t = typename remove_reference<T>::type;
} // namespace std
这样就可以省略 ::type
和 typename
,使代码变得简洁:
#include <type_traits>
int main() {
std::remove_reference_t<int > x{0}; // 等价于 int x{0};
std::remove_reference_t<int& > y{0}; // 等价于 int y{0};
std::remove_reference_t<int&&> z{0}; // 等价于 int z{0};
}
std::move
的实现定义在 <utility>
中的函数模板 std::move
用于将实参强制转换为右值引用。 借助于 std::remove_reference
可以给出它的一种实现:
#include <type_traits> // std::remove_reference_t
namespace std{
template <class T>
remove_reference_t<T>&& move(T&& t) {
// T 可能含有引用属性,先用 remove_reference_t 将其去除,
// 再用 static_cast 将所得类型强制转换为『右值引用』:
return static_cast<remove_reference_t<T>&&>(t);
}
} // namespace std
std::forward
的实现定义在 <utility>
中的函数模板 std::forward
用于完美转发实参,即保留函数实参的所有类型信息(含引用属性)。
#include <utility>
template<class T>
void foo_wrapper(T&& argu/* always lvalue */) {
foo(std::forward<T>(argu)); // Forward as lvalue or as rvalue, depending on T
}
借助于 std::remove_reference
和引用折叠机制可以给出它的一种实现:
#include <type_traits> // std::remove_reference_t
namespace std{
// 如果 T 为 int& ,则 remove_reference_t<T>& 及 T&& 均为 int& :
template <class T>
T&& forward(remove_reference_t<T>& t) {
return static_cast<T&&>(t);
}
// 如果 T 为 int&&,则 remove_reference_t<T>&& 及 T&& 均为 int&&:
template <class T>
T&& forward(remove_reference_t<T>&& t) {
return static_cast<T&&>(t);
}
} // namespace std
定义在 <type_traits>
中的编译期谓词 (compile-time predicate) 都是类模板。 它们都含有一个 static bool value
成员,可以用于对类型实参作编译期判断。 自 C++17 起,标准库为它们的 value
成员提供了以 _v
为后缀的别名 (alias),可以用于简化代码。
例如 std::is_empty
用于判断一个类的对象是否不占存储空间:
namespace std {
// is_empty 的声明:
template <class T> struct is_empty;
// C++17 引入的别名:
template <class T>
inline constexpr bool is_empty_v = is_empty<T>::value;
} // namespace std
用例:
#include <iostream>
#include <type_traits>
// 对象不占存储空间的类:
struct HasNothing { };
struct HasStaticDataMember { static int m; };
struct HasNonVirtualMethod { void pass(); };
// 对象占据存储空间的类:
struct HasNonStaticDataMember { int m; };
struct HasVirtualMethod { virtual void pass(); };
// 输出判断结果:
template <class T>
void print() {
std::cout << (std::is_empty_v<T> ? true : false) << ' ';
}
int main() {
print<HasNothing>();
print<HasStaticDataMember>();
print<HasNonVirtualMethod>();
print<HasNonStaticDataMember>();
print<HasVirtualMethod>();
}
运行结果:
1 1 1 0 0
编译期表达式 c ? v1 : v2
根据 c
的值(true
或 false
),从 v1
与 v2
中选取一个,作为该表达式的值。
定义在 <type_traits>
中的类模板 std::conditional
根据第一个(bool
型)模板实参的值,从后两个(类型)模板实参中选取一个:
#include <iostream>
#include <type_traits>
int main() {
using T = std::conditional<true, int, double>::type; // C++11
using F = std::conditional_t<false, int, double>; // C++14
static_assert(std::is_same_v<T, int>); // C++17
static_assert(std::is_same_v<F, double>); // C++17
}
一种可能的实现:
namespace std {
// 通用版本,用于 B == true 的情形:
template <bool B, class T, class F>
struct conditional { typedef T type; };
// 特化版本,用于 B == false 的情形:
template <class T, class F>
struct conditional<false, T, F> { typedef F type; };
// C++14 引入的别名:
template <bool B, class T, class F>
using conditional_t = typename conditional<B, T, F>::type;
} // namespace std
目前 (C++17),标准库没有提供从多个类型中选取一个的方法。 如果将来有这样的方法(暂且命名为 std::select
)被补充进标准库中,那么它大致应当支持如下用法:
#include <iostream>
#include <type_traits>
int main() {
using T2 = std::select<2, int, long, float, double>::type; // 仿 C++11
using T3 = std::select_t<3, int, long, float, double>; // 仿 C++14
static_assert(std::is_same_v<T2, float>); // C++17
static_assert(std::is_same_v<T3, double>); // C++17
}
在这里,std::select
以第一个模板实参为序号,从后面的模板实参列表中选出对应的类型。 它的实现需要用到类模板的特例化和递归以及变参模板等机制:
namespace std{
// 通用版本,禁止实例化
template<unsigned N, typename... Cases>
struct select;
// 特化版本 (N > 0):
template <unsigned N, typename T, typename... Cases>
struct select<N, T, Cases...> {
using type = typename select<N-1, Cases...>::type;
};
// 特化版本 (N == 0):
template <typename T, typename... Cases>
struct select<0, T, Cases...> {
using type = T;
};
// 标准库风格的类型别名:
template<unsigned N, typename... Cases>
using select_t = typename select<N, Cases...>::type;
} // namespace std
#include <type_traits>
struct Empty { };
template <bool C>
struct A {
int *pi; // 8
std::conditional_t<C, double, Empty> x; // C ? 8 : 1
};
template <bool C>
struct B {
int *pi; // 8
[[no_unique_address]] std::conditional_t<C, double, Empty> x; // C ? 8 : 0
};
int main() {
static_assert(sizeof(Empty) == 1);
static_assert(sizeof(A<true>) == 8 + 8);
static_assert(sizeof(B<true>) == 8 + 8);
static_assert(sizeof(A<false>) == 8 + 1 + 7/* padding */);
static_assert(sizeof(B<false>) == 8 + 0);
}
See Conditional Members and no_unique_address
for details.
元编程(编译期计算)中没有变量 (variable) 的概念,也没有循环 (loop) 机制,因此算法中用到的迭代 (iteration) 语义都必须通过递归 (recursion) 来实现的。
constexpr int factorial(int i) {
return i < 2 ? 1 : i * factorial(i-1);
}
int main() {
static_assert(factorial(0) == 1);
static_assert(factorial(1) == 1);
static_assert(factorial(2) == 2);
static_assert(factorial(3) == 6);
}
// 通用版本:
template <int I>
constexpr int factorial() {
return I * factorial<I-1>();
}
// 特化版本,用作『递归基』:
template <>
constexpr int factorial<0>() {
return 1;
}
// 测试:
int main() {
static_assert(factorial<0>() == 1);
static_assert(factorial<1>() == 1);
static_assert(factorial<2>() == 2);
static_assert(factorial<3>() == 6);
}
// 通用版本:
template <int I>
struct factorial {
static constexpr int value = I * factorial<I-1>::value;
};
// 特化版本,用作『递归基』:
template <>
struct factorial<0> {
static constexpr int value = 1;
};
// 仿 C++17 别名:
template <int I>
inline constexpr int factorial_v = factorial<I>::value;
// 测试:
int main() {
static_assert(factorial<0>::value == 1);
static_assert(factorial<1>::value == 1);
static_assert(factorial<2>::value == 2);
static_assert(factorial<3>::value == 6);
static_assert(factorial_v<0> == 1);
static_assert(factorial_v<1> == 1);
static_assert(factorial_v<2> == 2);
static_assert(factorial_v<3> == 6);
}
C++11 引入了变参模板 (variadic template),这种模板可以含有模板形参包 (template parameter pack),它是一种可以接受零个或多个模板实参 (template argument) 的特殊的模板形参 (template parameter)。
// Args 是一个『模板形参包』,可以接受零个或多个『模板实参』:
template <typename T, typename... Args>
// rest 是一个『函数形参包』,可以接受零个或多个『函数实参』:
void foo(const T& t, const Args&... rest);
在如下调用中
int i = 0;
double d = 3.14;
string s = "how now brown cow";
foo(i, s, 42, d); // 接受 3 个实参
foo(s, 42, "hi"); // 接受 2 个实参
foo(d, s); // 接受 1 个实参
foo("hi"); // 接受 0 个实参
编译器会生成以下 4
个版本的实例:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char(&)[3]);
void foo(const double&, const string&);
void foo(const char(&)[3]);
形参包的大小可以由 sizeof...
运算符获得:
template<typename... Args>
void g(Args... args) {
cout << sizeof...(Args) << endl;
cout << sizeof...(args) << endl;
}
该表达式是 constexpr
,因此不会对实参求值。
变参函数模板通常是递归的 (recursive)。 作为递归基的非变参版本必须在变参版本之前给出声明 (declaration):
#include <iostream>
// 『非变参』版本,用作『递归基』
template<typename T>
std::ostream& print(std::ostream& os, const T& t) {
return os << t;
}
// 『变参』版本,置于『非变参』版本之后:
template <typename T, typename... Args>
std::ostream& print(std::ostream& os, const T& t, const Args&... rest) {
os << t << ", ";
return print(os, rest...);
}
int main() {
print(std::cout, "hello", "world");
return 0;
}
位于形参包右侧的 ...
表示对这个包按相应的模式 (pattern) 作展开。在上面的变参版本中:
const Args&...
对类型形参包 Args
按模式 const Args&
作展开。rest...
对函数形参包 rest
按模式 rest
作展开。需要展开的形参包可以具有更加复杂的模式:
// 对 print 的每一个实参调用 Debug
template <typename... Args>
std::ostream& printDebug(std::ostream& os, const Args&... rest) {
// 相当于 print(os, Debug(a1), ..., Debug(an))
return print(os, Debug(rest)...);
}
上面定义的 print()
在末尾不执行换行。 若要添加一个末尾换行的版本,可以基于 print()
定义一个 println()
:
#include <iostream>
#include <utility>
// 『非变参』版本,用作『递归基』
template<typename T>
std::ostream& print(std::ostream& os, const T& t) {
return os << t;
}
// 『变参』版本
template <typename T, typename... Args>
std::ostream& print(std::ostream& os, const T& t, const Args&... rest) {
os << t << ", ";
return print(os, rest...);
}
// 末尾换行的版本
template <typename... Args>
void println(std::ostream& os, Args&&... args) {
print(os, std::forward<Args>(args)...);
print(os, '\n');
}
int main() {
println(std::cout, "hello", "world");
return 0;
}
在这里,std::forward<Args>(args)...
中的模板实参包 Args
和函数实参包 args
将同时被展开,相当于:
std::forward<T1>(t1), ..., std::forward<Tn>(tn)