IMPORTANT本文不再介绍 C++ 模板的基础用法.
模板也是「函数」
我们可以把模板也看成一种「函数」,这个「函数」的输入输出不仅限于值,还可以是类型,甚至「函数」.
案例 1统计若干类型的总大小.
#include <print>#include <vector>
template <class ...T>struct get_size {};
template <>struct get_size<> { static constexpr size_t value = 0; };
template <class T, class ...U>struct get_size<T, U...> { static constexpr size_t value = sizeof(T) + get_size<U...>::value;};
int main() { size_t N = get_size<int[1000], std::vector<int>, std::string>::value; std::println("1000 * 4 + 24 + 24 = {}", N);}1000 * 4 + 24 + 24 = 4048C++ 提供了能封装输出值的类型,不用我们手搓 value:
template <class ...T>struct get_size {};
template <>struct get_size<> : std::integral_constant<size_t, 0> {};
template <class T, class ...U>struct get_size<T, U...> : std::integral_constant<size_t, sizeof(T) + get_size<U...>::value> {};有没有函数式传参的写法?
错误的写法constexpr size_t get_size() { return 0; }template <class T, class ...U>constexpr size_t get_size(T t, U ...u) {return sizeof(t) + get_size(u...);}struct TypeA {};struct TypeB { TypeB(TypeB &&) = delete; };int main() {auto ret = get_size(std::declval<TypeA>(), std::declval<TypeB>());}
想要保持这种函数式写法,关键是不要真的传对象,而是传类型信息.
template <class T>struct Dummy { using type = T; };
template <class ...T>constexpr size_t get_size(Dummy<T> ...t) {
if constexpr (sizeof...(T) == 0) { return 0; } else { return (sizeof(t) + ...); // 折叠表达式实现递归 }}
int main() {
auto ret = get_size(Dummy<TypeA>{}, Dummy<TypeB>{});}关于constexpr函数若
constexpr函数中存在无法在编译期求值的参数,则constexpr函数和普通函数一样在运行时求值,此时的返回值不是常量表达式.如果想强制要求编译时求值,可以使用 C++20 的
consteval,运行时求值将报错.
二元 fold-expression 可以规定 sizeof...(T) 为 0 时的递归终止值,写法更简洁.另外自 C++20 开始,std::type_identity 可以代替手写 Dummy 包装类.
template <class ...T>consteval size_t get_size(std::type_identity<T> ...t) { return (sizeof(t) + ... + 0);}
template <class T>constexpr std::type_identity<T> tag{};
TypeA a{};auto ret = get_size(tag<decltype(a)>, tag<TypeB>);也可以使用模板显式传参的函数式写法:
template <class ...T>consteval size_t get_size() { return (sizeof(T) + ... + 0); }
auto ret = get_size<TypeA, TypeB>();案例 2实现
get_type<T, I>,要求:
- 如果
T为std::tuple<>,返回void;- 否则,如果
T为std::tuple<T0, ..., TN>:
- 如果
0 <= I && I <= N为true,返回TI;- 否则,返回
void;- 否则,返回
void.
#include <tuple>
template <class T, size_t I>struct get_type { using type = void; };
template <size_t I>struct get_type<std::tuple<>, I> { using type = void; };
template <class T0, class ...Ts>struct get_type<std::tuple<T0, Ts...>, 0> { using type = T0; };
template <class T0, class ...Ts, size_t I>struct get_type<std::tuple<T0, Ts...>, I> { // C++20 可以省略待决名的 typename using type = typename get_type<std::tuple<Ts...>, I - 1>::type;};
int main() { using tup = std::tuple<int, float, bool, char, void *>; using ret0 = get_type<tup, 0>::type; // int using ret1 = get_type<tup, 1>::type; // float using ret2 = get_type<tup, 2>::type; // bool using ret3 = get_type<tup, 3>::type; // char using ret4 = get_type<tup, 4>::type; // void *}待决名的typename何时可以省略自 C++20 开始,待决名的消歧义符
typename大部分都可以省略,大致除了:
- 非类型模板形参的默认值表达式中,如
template <class T, auto val = typename T::val_type{}>,即template <class T, T::val_type val = typename T::val_type{}>;- 模板实参类型,如
using t = Tup<typename Tmpl<Ts>::type...>;;- 函数形参类型,如
void func(typename T::val_type x) {...};- 在函数或者块作用域内变量/函数声明的类型,如
void func() { typename T::val_type val; }、void func() { typename T::val_type gunc(); }.
函数式写法:
template <class T, size_t I>consteval auto get_type_impl( std::type_identity<T>, std::integral_constant<size_t, I>) { return std::type_identity<void>{};}
template <class T0, class ...Ts, size_t I>consteval auto get_type_impl( std::type_identity<std::tuple<T0, Ts...>>, std::integral_constant<size_t, I>) { if constexpr (I == 0) { return std::type_identity<T0>{}; } else { return get_type_impl( std::type_identity<std::tuple<Ts...>>{}, std::integral_constant<size_t, I - 1>{} ); }}
template <class T, size_t I>using get_type = decltype(get_type_impl( std::type_identity<T>{}, std::integral_constant<size_t, I>{}))::type;
int main() { using tup = std::tuple<int, float, char>; using ret0 = get_type<tup, 0>; // int using ret1 = get_type<tup, 1>; // float using ret2 = get_type<tup, 2>; // char using ret3 = get_type<tup, 3>; // void}也可以不包装整型参数,从函数形参转移到模板形参:
template <size_t I, class T>consteval auto get_type(std::type_identity<T>) { return std::type_identity<void>{};}
template <size_t I, class T0, class ...Ts>consteval auto get_type(std::type_identity<std::tuple<T0, Ts...>>) { if constexpr (I == 0) { return std::type_identity<T0>{}; } else { return get_type<I - 1>(std::type_identity<std::tuple<Ts...>>{}); }}
int main() { using tup = std::tuple<int, float, char>; using ret0 = decltype(get_type<0>(std::type_identity<tup>{}))::type; // int using ret1 = decltype(get_type<1>(std::type_identity<tup>{}))::type; // float using ret2 = decltype(get_type<2>(std::type_identity<tup>{}))::type; // char using ret3 = decltype(get_type<3>(std::type_identity<tup>{}))::type; // void}包索引C++26 Pack Indexing 可以避免递归元编程:
template <size_t I, class ...Ts>consteval auto get_type(std::type_identity<std::tuple<Ts...>>) {if constexpr (I < sizeof...(Ts)) {return std::type_identity<Ts...[I]>{};} else {return std::type_identity<void>{};}}包索引表达式
...[]支持模板参数包、函数参数包、结构化绑定包、lambda 初始化捕获包.
案例 3现有类型
Tup<T1, ..., TN>,映射Tmpl: T -> U.实现模板mapping,要求:
- 返回
Tup<Tmpl<T0>, ..., Tmpl<TN>>.
#include <variant>
template <template <class> class Tmpl, class Tup>struct mapping {};
template < template <class> class Tmpl, template <class ...> class Tup, class ...Ts> struct mapping < Tmpl, Tup<Ts...>> { using type = Tup<typename Tmpl<Ts>::type...>;};
template <class T>struct copy10 { using type = T[10]; };
int main() { using t = std::variant<int, char, bool>;
using result_t = mapping<copy10, t>::type;}案例 4实现模板
array_wrapper: N -> (Tmpl: T -> T[N]).
#include <tuple>
// mapping 实现13 collapsed lines
template <template <class> class Tmpl, class Tup>struct mapping {};
template < template <class> class Tmpl, template <class ...> class Tup, class ...Ts> struct mapping < Tmpl, Tup<Ts...>> { using type = Tup<typename Tmpl<Ts>::type...>;};
template <size_t N>struct array_wrapper {
template <class T> struct Tmpl { using type = T[N]; };};
int main() { using t = std::tuple<int, char[10], bool>;
using result_t = mapping<array_wrapper<7>::Tmpl, t>::type;}type_traits
标准库提供了许多很有用的「函数」,不用我们手搓.
对于大多数操作,相应的 trait 有 noexcept 的版本,如 std::is_invocable_v<T, Args...> 对应于 std::is_nothrow_invocable_v<T, Args...>.
对于含特殊成员函数的类型,相应的 trait 有 trivial 的版本,如 std::is_constructible_v<T, Args...> 对应于 is_trivially_constructible_v<T, Args...>.
类型判断
类别
| Traits | Desc |
|---|---|
std::is_fundamental_v<T> | 检查 T 是否为基本类型 1 |
std::is_void_v<T> | 检查 T 是否为 void |
std::is_null_pointer_v<T> | 检查 T 是否为 std::nullptr_t 2 |
std::is_arithmetic_v<T> | 检查 T 是否为算术类型 3 |
std::is_integral_v<T> | 检查 T 是否为整型 4 |
std::is_floating_point_v<T> | 检查 T 是否为浮点类型 |
std::is_enum_v<T> | 检查 T 是否为枚举类型 |
std::is_pointer_v<T> | 检查 T 是否为指针类型 5 |
std::is_member_object_pointer_v<T> | 检查 T 是否为成员变量指针类型 |
std::is_member_function_pointer_v<T> | 检查 T 是否为成员函数指针类型 |
std::is_reference_v<T> | 检查 T 是否为引用类型 |
std::is_lvalue_reference_v<T> | 检查 T 是否为左值引用类型 |
std::is_rvalue_reference_v<T> | 检查 T 是否为右值引用类型 |
属性
| Traits | Desc |
|---|---|
std::is_const_v<T> | 检查 T 是否带有 const 限定 |
std::is_volatile_v<T> | 检查 T 是否带有 volatile 限定 |
std::is_empty_v<T> | 检查 T 是否为空类型 6 |
std::is_polymorphic_v<T> | 检查 T 是否为多态类 |
std::is_abstract_v<T> | 检查 T 是否为抽象类 |
std::is_final_v<T> | 检查 T 是否被 final 修饰 |
std::is_trivially_copyable_v<T> | 检查 T 是否为可平凡复制类型 7 |
std::is_trivial_v<T> | 检查 T 是否为平凡类 8 |
std::is_standard_layout_v<T> | 检查 T 成员的内存布局是否为标准布局 9 |
std::is_aggregate_v<T> | 检查 T 是否为聚合类型 10 |
std::is_scoped_enum_v<T> | 检查 T 是否为作用域枚举类型 |
std::is_signed_v<T> | 检查 T 是否为有符号的算术类型 |
std::is_unsigned_v<T> | 检查 T 是否为无符号的算术类型 |
std::is_bounded_array_v<T> | 检查 T 是否为已知大小的数组类型 |
std::is_unbounded_array_v<T> | 检查 T 是否为未知大小的数组类型 |
可支持的操作
| Traits | Desc |
|---|---|
std::is_swappable_with_v<T, U> | 检查 T 和 U 是否可交换 11 |
std::is_swappable_v<T> | 检查 T 之间是否可交换 12 |
std::is_constructible_v<T, Args...> | 检查 Args... 能否构造出 T 13 |
std::is_default_constructible_v<T> | 检查 T 能否默认构造 |
std::is_copy_constructible_v<T> | 检查 T 能否拷贝构造 |
std::is_move_constructible_v<T> | 检查 T 能否移动构造 |
std::is_assignable_v<T, U> | 检查 U 能否赋值给 T 14 |
std::is_copy_assignable_v<T> | 检查 T 能否拷贝赋值 |
std::is_move_assignable_v<T> | 检查 T 能否移动赋值 |
std::is_destructible_v<T> | 检查 T 能否析构 15 |
std::has_virtual_destructor_v<T> | 检查 ~T() 是否为虚函数 |
std::is_invocable_v<T, Args...> | 检查能否以参数类型 Args... 调用 T 16 |
std::is_invocable_r_v<Ret, T, Args...> | 同上,并检查返回类型是否为 Ret |
类型关系
| Traits | Desc |
|---|---|
std::is_same_v<T, U> | 检查 T, U 是否相同 17 |
std::is_convertible_v<From, To> | 检查 From 能否隐式转换 18 成 To 19 |
std::is_base_of_v<Base, Derived> | 检查 Base 是否为 Derived 的基类 |
std::is_virtual_base_of_v<Base, Derived> (C++26) | 检查 Base 是否为 Derived 的虚基类 |
std::is_layout_compatible_v<T, U> | 检查 T, U 在内存布局上是否兼容 |
std::is_pointer_interconvertible_base_of_v<B, D> | 检查 D * 能否安全转成 B * 20 |
std::is_pointer_interconvertible_with_class(M S::*mp) | 检查 S * 能否安全转成 M * 21 |
类型查询
| Traits | Desc |
|---|---|
std::alignment_of_v<T> | 查询 T 的对齐字节数,即 alignof(T) |
std::rank_v<T> | 查询 T 作为多维数组时的维数 |
std::extent_v<T, N> | 查询 T 作为多维数组时在第 N 维的元素数量 22 |
std::invoke_result_t<F, Args...> | 查询以参数类型 Args... 调用 T 返回的类型 |
类型处理
| Traits | Desc |
|---|---|
std::add_const_t<T> | T -> const T |
std::add_volatile_t<T> | T -> volatile T |
std::add_cv_t<T> | T -> const volatile T |
std::add_lvalue_reference_t<T> | T -> T & |
std::add_rvalue_reference_t<T> | T -> T && |
std::add_pointer_t<T> | T -> T * |
std::remove_const_t<T> | const T -> T |
std::remove_volatile_t<T> | volatile T -> T |
std::remove_cv_t<T> | const volatile T -> T |
std::remove_reference_t<T> | T &/&& -> T |
std::remove_cvref_t<T> | const volatile T &/&& -> T |
std::unwrap_reference_t<T> | std::reference_wrapper<T>/T -> T/T |
std::unwrap_ref_decay_t<T> | std::reference_wrapper<T>/T -> T/std::decay_t<T> |
std::decay_t<T> | U[N] -> U * / U(...) -> U(*)(...) / cv U& -> U |
std::remove_extent_t<T[N]> | T[N] -> T 移除一个维度 |
std::remove_all_extents_t<T[I]...[N]> | T[I]...[N] -> T 移除所有维度 |
std::remove_pointer_t<T *> | T * -> T,非指针类型 T -> T |
std::common_reference_t<T...> | 转换成 T... 的公共类型 |
std::enable_if_t<bool, T> | T -> T/,若 false 则代换失败,用于 SFINAE |
std::conditional_t<bool, T, F> | T -> T/F |
std::void_t<T...> | T... -> void,用于 SFINAE |
std::type_identity_t<T> | T -> T,用于非推断语境 |
Trait 运算
| Traits | Desc |
|---|---|
std::integral_constant<T, v> | 把 v 封装进 static constexpr T value,包装输出 |
std::bool_constant<b> | std::integral_constant<bool, b> |
std::true_type / std::false_type | std::bool_constant<true / false> |
std::conjunction_v<B...> | 对 B... 进行逻辑合取 23 |
std::disjunction_v<B...> | 对 B... 进行逻辑析取 24 |
std::negation_v<B> | 对 B 进行逻辑非 25 |
约束类型
利用 SFINAE 约束类型
在模板实参推导或函数类型推导中,代换失败并不是错误,编译器仅在重载集中抛弃该特化,不会编译失败.在 C++20 之前,我们可以利用这一点对模板类型参数进行约束.
这里的代换失败,发生在 immediate context 中,即
- 模板函数模板形参中的默认实参类型/表达式
- 模板函数形参类型
- 模板函数返回值类型
- 模板类模板形参中的默认实参类型/表达式
- 模板类模板参数列表里的类型/表达式
函数模板签名里直接写到的类型/表达式都会出现在 immediate context.在这个语境因代换失败发生的错误,才被称为 SFINAE error.如果是触发了别的模板实例化、隐式成员生成等连锁反应而间接导致的错误是 hard error (会导致编译错误).
最经典的例子就是
<..., class = typename B<T>::type>,要想取出type,必须实例化B<T>,一旦实例化失败 (比如B<T>不含type),则发生 hard error.
案例 1对
template <size_t N> void foo() {}中的N约束:
N为偶数
根据 SFINAE error 触发的位置和方式,可以有下面几种写法:
// 1template <size_t N>void foo(char (*)[N % 2 == 0] = nullptr) { std::println("N is even");}
// 2template <size_t N>void foo(std::enable_if_t<N % 2 == 0, int> = 0) { std::println("N is even");}
// 3template <size_t N>auto foo() -> decltype(std::declval<int[N % 2 == 0]>(), void()) { std::println("N is even");}
// 4template <size_t N, std::enable_if_t<N % 2 == 0, int> = 0>void foo() { std::println("N is even");}利用 Concept 约束类型
CRTP
CRTP (Curiously Recurring Template Pattern) 可以在编译期间把派生类的类型作为模板参数传递给基类.
实现「静态多态」/「接口约定」
所谓「静态多态」就是在编译期确定行为绑定,通过统一接口适配不同类型的具体实现.函数重载就是一种「静态多态」.
CRTP 通过模板参数将派生类型注入基类,使得基类能在编译期调用派生类的具体实现,从而实现基类定义接口、派生类提供实现的「静态多态」.
CRTP 让派生类的基类各不相同,失去了在抽象层处理的能力,比如将不同派生类型的对象放入基类容器里、通过基类指针调用具体派生类实现等;而动态多态就可以,并能在运行时确定具体派生类型,从而调用具体派生类的实现.
另一种角度上面是网上很多人使用的表述.也可以这么认为:CRTP 起到使不同类存在若干相同接口的成员函数的约定作用,没有什么继承关系.
template <class T>class Weapon {protected: float power;
public: void attack() { auto that = static_cast<T*>(this); if (that->can_level_up()) { that->fix_impl(); power += that->value; } that->attack_impl(); }
void fix() { auto that = static_cast<T*>(this); that->fix_impl(); }};
class Bow : public Weapon<Bow> { friend class Weapon<Bow>;protected: int value;public: bool can_level_up() { /* ... */ } void fix_impl() { /* ... */ } void attack_impl() { /* ... */ }};
class Gun : public Weapon<Gun> { friend class Weapon<Gun>;protected: int value;public: bool can_level_up() { /* ... */ } void fix_impl() { /* ... */ } void attack_impl() { /* ... */ }};
template <class T>void fix_weapon(Weapon<T> &wp) { wp.fix(); }
int main() {
std::vector<std::unique_ptr<Weapon>> wps;}C++23 可以向非静态成员函数显式传入对象自身,在此之前都是通过隐式传入 this 指针实现的.
struct A { void f(int x, int y) const; // void f(int, int) const &; void g(float x, bool y); // void g(float, bool) &;
void func(this A &self, int x); // void func(int) &; void func(this A const &self, int x); // void func(int) const; void func(this A &&self, int x); // void func(int) &&; void func(this A const &&self, int x); // void func(int) const &&; void gunc(this A self, float y); // 按值传入对象自身
// void gunc(this A &self, float y); // 会和 gunc(this A, float) 歧义 void hunc(this A &self) { // void hunc() &; // this->func(7); // 使用 this-deducing 后无法再使用 this self.func(7); // this->func(7) / func(7) }};
A a;A const ca;A *pa = &a;A const *pca = &ca;
void (A::*pf)(int, int) const = &A::f; // 成员函数指针void (*pfunc)(A const &, int) = &A::func; // 普通函数指针
a.f(4, 9); (a.*pf)(4, 9); (pa->*pf)(4, 9); std::invoke(pf, a, 4, 9);// pf(a, 4, 9); pf(pa, 4, 9); // pf 是成员函数指针,需 std::invoke 调用ca.func(7); pfunc(ca, 7); std::invoke(pfunc, ca, 7);// (ca.*pfunc)(7); (pca->*pfunc)(7); // pfunc 是普通函数指针
struct B { template <class T> void f(this T& self) // & / const & template <class T> void func(this T&& self); // & / const & / && / const && void gunc(this auto&& self); // 同上,转发引用};self 是实打实的派生对象而不是基类指针,这意味着我们不再需要把指针类型转换到实际派生类型了.
class Weapon {protected: float power;
public: void attack(this auto&& self) { if (self.can_level_up()) { self.fix_impl(); self.power += self.value; } self.attack_impl(); }
void fix(this auto&& self) { self.fix_impl(); }};
class Bow : public Weapon { friend class Weapon;protected: int value;public: bool can_level_up() { /* ... */ } void fix_impl() { /* ... */ } void attack_impl() { /* ... */ }};
class Gun : public Weapon { friend class Weapon;protected: int value;public: bool can_level_up() { /* ... */ } void fix_impl() { /* ... */ } void attack_impl() { /* ... */ }};
template <class T> requires std::is_base_of_v<Weapon, T>void fix_weapon(T &wp) { wp.fix(); }
int main() { Bow a; fix_weapon(a);
std::unique_ptr<Weapon> b = std::make_unique<Gun>(); // fix_weapon(*b); std::vector<std::unique_ptr<Weapon>> v;}实现「注入实现」
CRTP 的另一个作用就是复用代码的通用逻辑,实现自动化定义成员函数.
案例 1现有基类
Planet以及派生类Sun,Earth,对这两个派生类使用 单例模式.
struct Planet { float mass, radius, heat; float get_volume() const { return 3.14f * 4 / 3 * radius * radius * radius; }};
class Sun : public Planet {private: Sun() { /* ... */ }; ~Sun() { /* ... */ }; Sun(Sun const &) = delete; Sun& operator=(Sun const &) = delete;
public: static Sun& get_instance() { static Sun instance = Sun(); return instance; }
void light(Planet& p) { float dH = heat / get_volume(); p.heat += dH; heat -= dH; }};
class Earth : public Planet {private: Earth() { /* ... */ }; ~Earth() { /* ... */ }; Earth(Earth const &) = delete; Earth& operator=(Earth const &) = delete;
public: static Earth& get_instance() { static Earth instance = Earth(); return instance; }};我们发现这种 单例模式 的逻辑高度通用.
失败的实践尝试使用协议类的形式提取这种逻辑:
class Singleton {protected:Singleton() = default;~Singleton() = default;Singleton(Singleton const &) = delete;Singleton& operator=(Singleton const &) = delete;public:static Singleton& get_instance() {static Singleton instance = Singleton();return instance;}};class Sun : public Singleton, public Planet {protected:Sun() { /* ... */ }~Sun() { /* ... */ }public:void light(Planet& p) {float dH = heat / get_volume();p.heat += dH;heat -= dH;}};class Earth : public Singleton, public Planet {protected:Earth() { /* ... */ }~Earth() { /* ... */ }};int main() {// 1Sun& s = Sun::get_instance();Sun&& t = std::move(Sun::get_instance());// Sun my_sun, another_sun;}最大的问题就是协议类的单例模式根本没有应用在派生类上:
Sun::get_instance()得到的是Singleton的单一实例而非Sun的单一实例.我们提取逻辑的同时,类型信息也被抹去了.
我们只是想复用「禁止拷贝,通过 get_instance() 获取唯一实例」这种模式,并在类 Singleton 统一实现这种模式,但又缺乏应用该模式的类型信息.如果实现内容仅类型不同,这个时候就能使用 CRTP 实现「自动化实现」:
template <class T>class Singleton {protected: Singleton() = default; ~Singleton() = default;
Singleton(Singleton const &) = delete; Singleton& operator=(Singleton const &) = delete;public: static T& get_instance() { static T instance = T(); return instance; }};
class Sun : public Singleton<Sun>, public Planet {
friend class Singleton<Sun>;protected: Sun() { /* ... */ } ~Sun() { /* ... */ }public: void light(Planet& p) { float dH = heat / get_volume(); p.heat += dH; heat -= dH; }};
class Earth : public Singleton<Earth>, public Planet { friend class Singleton<Earth>;protected: Earth() { /* ... */ } ~Earth() { /* ... */ }};另一种视角?也许可以这样理解:CRTP 允许构建一个特别的「函数」,输入是类型、值、「函数」,输出是实现,并把输出 注入 到派生类里.
案例 2现有 4 个类
Ball、Cube、Cat、Dog,其中Ball、Cube的基类是Object,Cat、Dog的基类是Animal.实现:
Object的深拷贝方法;Animal的深拷贝方法.
我们的拷贝操作发生在上层模块,不知道待拷贝对象的具体类型,如果直接调用上层拷贝构造函数,会发生 对象切片 (object-slicing),丢失类型信息.
假设现在有
Ball x; Cube y; Cat z; Dog w;浅拷贝返回值实际上指向的还是原来的对象,没有真正拷贝:
struct Object { float object_data; std::unique_ptr<Object> clone() { return std::unique_ptr<Object>(this); }};
struct Animal { float animal_data; std::unique_ptr<Animal> clone() { return std::unique_ptr<Animal>(this); }};
struct Ball : Object { float ball_data; };struct Cube : Object { float cube_data; };struct Cat : Animal { float cat_data; };struct Dog : Animal { float dog_data; };而深拷贝的 new 操作又必须要求得知具体类型:
std::unique_ptr<Object> ret;ret = std::make_unique<Ball>(x);ret = std::make_unique<Cube>(y);ret = std::make_unique<Cat>(z);ret = std::make_unique<Dog>(w);虽然深拷贝成功,但显式写出了对象的具体类型,违反了开闭原则.
如果愿意,可以保留虚函数接口,把实现下放:
struct Object { float object_data; virtual ~Object() = default; virtual std::unique_ptr<Object> clone() = 0;};
struct Animal { float animal_data; virtual ~Animal() = default; virtual std::unique_ptr<Animal> clone() = 0;};
struct Ball : Object { float ball_data; std::unique_ptr<Object> clone() override { return std::make_unique<Ball>(*this); }};
struct Cube : Object { float cube_data; std::unique_ptr<Object> clone() override { return std::make_unique<Cube>(*this); }};
struct Cat : Animal { float cat_data; std::unique_ptr<Animal> clone() override { return std::make_unique<Cat>(*this); }};
struct Dog : Animal { float dog_data; std::unique_ptr<Animal> clone() override { return std::make_unique<Dog>(*this); }};超级复读机!我们可以使用 CRTP 提取这种通用的逻辑,直接在基类实现它们!
错误的实践template <class Derived>struct Object {float object_data;std::unique_ptr<Object> clone() {auto that = static_cast<Derived *>(this);return std::make_unique<Derived>(*that);}};template <class Derived>struct Animal {float animal_data;std::unique_ptr<Animal> clone() {auto that = static_cast<Derived *>(this);return std::make_unique<Derived>(*that);}};struct Ball : Object<Ball> { float ball_data; };struct Cube : Object<Cube> { float cube_data; };struct Cat : Animal<Cat> { float cat_data; };struct Dog : Animal<Dog> { float dog_data; };CRTP 的副作用就是让基类变成了模板,导致派生类的基类各不相同,失去了动态派发的能力:
Ball x; Cube y;std::shared_ptr<Object<Ball>> nx = x.clone(); // are you kidding me?std::shared_ptr<Object<Cube>> ny = y.clone(); // 这还不是违反了开闭原则?
要让基类 Base 免受 CRTP 的副作用,保留基类指针指向派生类对象的能力,我们可以试试套一层辅助类 BaseImpl:
还不完全正确的实践struct Object { float object_data; };template <class Derived>struct ObjectImpl : Object {std::unique_ptr<Object> clone() {auto that = static_cast<Derived *>(this);return std::make_unique<Derived>(*that);}};struct Animal { float animal_data; };template <class Derived>struct AnimalImpl : Animal {std::unique_ptr<Animal> clone() {auto that = static_cast<Derived *>(this);return std::make_unique<Derived>(*that);}};struct Ball : ObjectImpl<Ball> { float ball_data; };struct Cube : ObjectImpl<Cube> { float cube_data; };struct Cat : AnimalImpl<Cat> { float cat_data; };struct Dog : AnimalImpl<Dog> { float dog_data; };你会发现的确能够用基类指针指向派生类对象,但本质上你还是失去了多态的
clone:Ball x;std::unique_ptr<Object> nx = x.clone();std::unique_ptr<Object> nnx = nx->clone();
因此若想保留动态多态,基类 Base 的虚函数接口无论如何也不能丢,然后在 BaseImpl 辅助类里利用 CRTP 实现虚函数!
struct Object { float object_data; virtual ~Object() = default; virtual std::unique_ptr<Object> clone() = 0;};
template <class Derived>struct ObjectImpl : Object { std::unique_ptr<Object> clone() override { auto that = static_cast<Derived *>(this); return std::make_unique<Derived>(*that); }};
struct Animal { float animal_data; virtual ~Animal() = default; virtual std::unique_ptr<Animal> clone() = 0;};
template <class Derived>struct AnimalImpl : Animal { std::unique_ptr<Animal> clone() override { auto that = static_cast<Derived *>(this); return std::make_unique<Derived>(*that); }};
struct Ball : ObjectImpl<Ball> { float ball_data; };struct Cube : ObjectImpl<Cube> { float cube_data; };struct Cat : AnimalImpl<Cat> { float cat_data; };struct Dog : AnimalImpl<Dog> { float dog_data; };NOTE举个例子,
AnimalImpl<Cat>往Cat里注入了std::unique_ptr<Animal> clone(),Derived对应Cat,减少了复读机的感觉.这个时候 CRTP 显然不是为了消除虚表开销,CRTP 并非与virtual水火不容.
感觉还是有点复读机,还能再提取吗?只要想保留动态多态,通过 CRTP 注入 clone 实现的方式并不能 override 基类的 clone,override 就是无法避免的,就算再写一个 Copyable<Base, Derived>::copy_impl 注入,也意义不大了.
编译期 for 循环
Footnotes
-
基本类型 包括可带 cv 限定的
void、可带 cv 限定的std::nullptr_t、整型 13、浮点型,与 复合类型std::is_compound_v<T>相对. ↩ -
nullptr是std::nullptr_t的实例.
std::nullptr_t并不是指针类型,即std::is_pointer_v<std::nullptr_t> -> false;
std::nullptr_t可 (显式/隐式) 转换成指针类型,如void *,char const *;
std::nullptr_t不能在模板中做类型推导std::nullptr_t -> T*. ↩ -
只能检测普通指针、函数指针、成员指针,不能检测智能指针,需自定义 trait,如:
template <class T> struct is_smart<std::shared_ptr<T>> : std::true_type {};↩ -
空类型不含非静态成员变量、虚函数、虚基类,如
struct { static int x; int f(){} }. ↩ -
平凡类型 包括标量类型 26、平凡类、这些类型的数组、cv 限定版本.
称一个类 可平凡复制,当且仅当其 (复制/移动)(构造函数/赋值运算符)、析构函数都是默认的,且无虚函数、虚基类.
可平凡复制类型的内存分布是连续的,因此可以通过std::memcpy按字节将对象序列化成unsigned char []或std::byte [].虽然内存连续,但内存布局和 C 语言不同,成员顺序由编译器决定 (比如访问权限相同的成员变量可能重新放在一起,C 语言不认识这些访问说明符),因此不能兼容 C 程序.\ ↩ -
标准布局类型 包括标量类型 26、标准布局类、这些类型的数组、cv 限定版本.
一个类如果无虚函数、虚基类,所有非静态数据成员都具有相同的访问说明符,在继承体系中最多只有一个类中有非静态数据成员,在继承体系中所有类的第一个非静态成员的类型与其基类不同 (在 C++ 中,空基类的地址与第一个非静态成员共享.一旦相同,由于标准规定相同类型的对象地址必须不同,势必多分配出空间以存储基类地址,内存布局发生变化),这时类的内存布局 (成员在内存中的排列方式) 与 C 语言的结构体完全一致,也称标准布局,这个类也被称为 标准布局类.标准布局类允许用户自定义成员函数.
如果平凡类型使用标准内存布局,则称这个类型为 POD 类型,与 C 语言的类型无缝衔接,相应的 trait 为std::is_pod_v<T>,该 trait 在 C++20 中弃用,取而代之的是std::is_trivial_v<T> && std::is_standard_layout_v<T>. ↩ -
聚合类型 可以是数组,也可以是一个类:这个类没有自定义构造函数,所有非静态成员都是
public的,无虚函数、虚基类,继承只能是公有继承.在 C++17 之前,这个类不能有基类.聚合类型的特色便是可以聚合初始化、在结构化绑定中可分解. ↩ -
返回
true,当且仅当std::swap(std::declval<T>(), std::declval<U>())在不求值语境中是良构的. ↩ -
等价于
std::is_swappable_with_v<T&, T&>,只要T是可引用的类型 (其实就是非void),就返回true. ↩ -
返回
true,当且仅当T obj(std::declval<Args>()...);在不求值语境中是良构的. ↩ ↩2 -
返回
true,当且仅当std::declval<T>() = std::declval<U>()在不求值语境中是良构的. ↩ -
T是引用类型,或者std::declval<U&>().~U()在不求值语境中是良构的,其中U是T移除所有多维数组维度后的类型,即using U = std::remove_all_extents_t<T>;,返回true;对于可带 cv 限定的void、函数类型、未知边界的数组,返回false. ↩ -
只要是能被
std::invoke调用的类型都被称为invocable,如函数、函数指针、成员函数指针、仿函数、lambda 表达式 (匿名仿函数)、std::function对象、std::bind对象.std::is_invocable_v<T>仅检查语法上能否调用,可触发隐式转换. ↩ -
cv 限定符也要完全相同. ↩
-
隐式转换 主要发生在算术类型的标准转换、cv 限定的添加、从派生类到基类的指针/引用转换、从基类到派生类的成员 (变量/函数) 指针转换、从数组/函数到指针的类型退化、用户定义的非
explicit单参构造函数、用户定义的explicit转换函数、函数实参到形参的转换、函数返回值到返回类型的转换. ↩ -
若
From和To都是可能带 cv 限定的void,则返回true. ↩ -
D必须是标准内存布局 9,才能保证从D *转来的B *仍能正常工作,否则D的首地址将存放非空基类B的信息.此外如果D和B相同,返回true. ↩ -
s.*mp(即s.m) 与s首地址重合,相当于S必须是标准内存布局 8,否则首地址将存放非空基类信息.此时reinterpret_cast<M&>(s)能唯一指代s.m.此外要返回true,M得是对象类型,mp是非空指针. ↩ -
若相应维度的元素数量未知,则返回
0. ↩ -
若
sizeof...(B)为0,则std::conjunction<B...>继承std::true_type,否则继承第一个bool(B::value)为false的B,若bool(B::value) && ...为true,继承最后一个B.B通常是std::true_type / std::false_type. ↩ -
若
sizeof...(B)为0,则std::disjunction<B...>继承std::false_type,否则继承第一个bool(B::value)为true的B,若bool(B::value) || ...为false,继承最后一个B.B通常是std::true_type / std::false_type. ↩ -
继承
std::bool_constant<!bool(B::value)>. ↩ -
标量类型 包括算术类型 3、枚举类型、指针类型、成员指针类型、
std::nullptr_t、这些类型的 cv 限定版本,对应的 trait 为std::is_scalar_v<T>. ↩ ↩2