Skip to main content

模版元入门

· 19 min read

模版类型推导

template<typename T>
void f(ParamType param);

f(expr)

对上面这种形式,依据ParamType可以分为三种情况

  1. ParamType是指针或引用类型,但不是通用引用(Universal Reference)【通用引用应该是modern effective c++的作者提出来的概念,表示模版中&&针对传入的是左值还是右值提供不同的效果,本质cpp只有引用折叠】
  2. ParamType是通用引用(Universal Reference)
  3. ParamType既不是指针也不是引用

ParamType是指针或普通引用类型

/// 引用
template<typename T>
void f(T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T is int, ParamType is int&
f(cx); // T is const int, ParamType is const int&
f(rx); // T is const int, ParamType is const int&


const修饰会跟随传递进模版参数T;但是引用修饰会被忽略(指针与引用类似)

template<typename T>
void f(const T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T is int, ParamType is const int&
f(cx); // T is int, ParamType is const int&
f(rx); // T is int, ParamType is const int&

ParamType是通用引用

templat<typename T>
void f(T&& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // x is lvalue, T is int&, ParamType is int&
f(cx); // x is lvalue, T is const int&, ParamType is const int&
f(rx); // x is lvalue, T is const int&, ParamType is const int&
f(27); // x is rvalue, T is int, ParamType is int&&

如果传入f的是个左值,T和ParamType都会被推导为左值引用

如果传入f的是右值, 规则同case1

引用折叠?&传给 && 或者 && 传给& 都会变成&; && 传给&&还是&&

ParamType不是引用/指针

templat<typename T>
void f(T param);

int x = 27;
const int cx = x;
const int& rx = x;
int* y = x;

f(x); // T is int, ParamType is int
f(cx); // T is int, ParamType is int
f(rx); // T is int, ParamType is int
f(y); // T is int*, ParamType is int*

传引用,值传递,函数中的T相当于是个深拷贝,因此T不会带const

传指针进去会解析成指针类型

struct A {
int x = 0;
};


template<typename T>
void f1(T a){
a.x = 1;
}

template<typename T>
void f2(T& a){
a.x = 2;
}

int main() {
A a;
A& a1 = a;
A& a2 = a;

// auto a11 = a; auto = A, a1 is A
f1(a1);
std::cout << a1.x << std::endl;

// auto& a22 = a; auto = A, a22 is A&
f2(a2);
std::cout << a2.x << std::endl;
}

auto类型推导

auto type deduction is template type deduction

// auto x = 27; 等价于
template<typename T>
void f(T param);
f(27); // auto = T = int

// const auto cx = x; 等价于
template<typename T>
void f(const T param);
f(cx); // auto = T = int

// const auto& rx = x;
template<typename T>
void f(const T& param);
f(rx); // auto = T = int

// auto&& y = std::move(x)
template<typename T>
void f(T&& param);
f(y); // auto = T = int, ParamType = int&&

为什么下面两个都可以

int main() {
auto a1 = new A; // case 3
delete a1;

auto* a2 = new A; // case 1
delete a2;
}

模版参数

模版参数主要有三种

  1. 类型
  2. 整数和整数的衍生类型
  3. 模版

类型模版参数

template<typename T>
void print(const T& t){
std::cout << t << std::endl;
}

print(1);
int x = 1;
print(x);

整数

必须编译期确定的值

template <int* p>
void f(int data){
*p = data;
}

int g = 1;

struct A{
int m1;
static int m2;
};

int A::m2 = 1;

int main(){
f<&g>(0);
f<&A::m2>(0);

static int c = 3;
f<&A::m2>(0);
}

整数衍生类型

地址在编译期肯定不知道,但是只要保证某个地址的数据在运行期间是不变的,就可以用这种方式

template <int* p>
void f(int data){
*p = data;
}

函数指针

因为函数地址在程序段,执行过程就不会变,所以可以用作模版参数

//
// Created by ChenhuiWang on 2024/3/28.

// Copyright (c) 2024 Tencent. All rights reserved.
//
#include "iostream"
#include <vector>

template <void (*func)()>
void f(){
func();
}

void f1(){}

struct A{
void f2(){}
static void f3(){}
};

int main(){
constexpr void (*pf1)() = &f1;

f<&f1>();
f<pf1>();
f<&A::f2>(); //error: &A::f2的类型是void (Test::*)()
f<&A::f3>();
}

模版类型(模版嵌套)

template <template<typename, typename> typename Tem>
void f(){
Tem<int, std::string> te;
te.show();
}

template<typename T1, typename T2>
struct A1{
void show(){
std::cout << 1 << std::endl;
}
};

template<typename T1, typename T2>
struct A2{
void show(){
std::cout << 2 << std::endl;
}
};

int main(){
f<A1>(); //A1实例化前在是template类型
f<A2>();
}

变参模版

template<typename... Ts>
auto sum(Ts... ts){
return (ts + ...);
}

int main(){
sum(1, 2);
sum(1.0, 2);
}

typename... 表示包pack, 包名为Ts, Ts... 表示对Ts进行解包, 必须在参数列表的最后。

Ts可以为空

解包方法

  1. 直接展开,包会被编译器展开为逗号间隔的参数
std::byte memory[16384];
void* head = memory;

template<typename T, typename... Args>
T& create(Args... args){
auto& obj = *new(head) T(args...); // use operator new
head += sizeof(T);
return obj;
}

struct A{
A(int, double){}
}

int main(){
auto a = Create<A, int, double>(1, 2.5);
}

// 模版实例化后会这样
A& create(int arg1, double arg2){
auto& obj = *new(head) A(arg1, arg2);
head += sizeof(T);
return obj;
}

  1. 嵌套展开
template<typename T, int... N>
std::vector<T> GetSubVector(const std::vector<T>& src){
return std::vector<T>{src.at(N)...}
}
  1. 按照符号展开(c++ 17)折叠表达式
// c++17之前
template<typename T>
auto sum(T arg){
return arg;
}

template<typename T, typename... Ts>
auto sum(T t, Ts... ts){
return t + sum<Ts...>(ts);
}

sum<int, double, float, short>(1, 1.0, 2.0, 3);
==> 1 + sum<double, float, short>(1.0, 2.0, 3);
==> 1 + 1.0 + sum<float, short>(2.0, 3);
==> 1 + 1.0 + 2.0 + sum<short>(3);
==> 1 + 1.0 + 2.0 + 3

// c++17 之后
template<typename T>
auto sum(T... ts){
// 三个点在左边, 表示从左到右合并
// 折叠表达式必须要有括号
return (... + ts);
}

模版链接

写在header中的原因

// a.h
template<typename T>
void f();

// a.cpp
// 如果把这个写进a.h就不会报错了,并且如果a.h被多文件include,不会产生multi defined
// 因为未实例化的模版是自动inline的
// 注意全特化的话就不会自动inline了,他是实例化后的模版函数
template<typename T>
void f(){

}

// main.cpp
#include <a.h>
int main(){
f<int>();
}


产生两个编译单元 main.o和a.o

main.o中void f<int>(); 去其他地方找实现,但是a.o中没有使用f<int>就不会产生相应的函数被链接了。

模版显示实例化

// a.h
template<typename T>
void f();

// a.cpp
template<typename T>
void f(){}

// 显式实例化
template f<int>();

// main.cpp
#include <a.h>
int main(){
f<int>();
}

SFINAE

Substitution Failure Is Not An Error

//
// Created by ChenhuiWang on 2024/3/29.

// Copyright (c) 2024 Tencent. All rights reserved.
//
#include <iostream>

template<typename T, bool Condition>
struct EnableIf{};

template<typename T>
struct EnableIf<T, true>{
using type = T;
};

template<typename T>
void f(typename EnableIf<T, sizeof(T) <= sizeof(void*)>::type t){
std::cout << "1" << std::endl;
}

template<typename T>
void f(typename EnableIf<T, (sizeof(T) > sizeof(void*))>::type t){
std::cout << "2" << std::endl;
}

struct A{
int data[16];
};
void Demo(){
f<int>(1);
A a;
f<A>(a);
}

//实例化过程,进行模版匹配
f<int>(1)
==> void f(typename EnableIf<T, sizeof(T) <= sizeof(void*)>::type t)
==> 命中EnableIf的EnableIf<T, true>的特化版本
==> f(int)

f<A>(a)
==> void f(typename EnableIf<T, sizeof(T) <= sizeof(void*)>::type t)
==> 没有这个特化版本,回归到通用模版template<typename T, bool Condition> struct EnableIf{};
==> 通用版本没有::type, 匹配失败
==> 继续匹配 void f(typename EnableIf<T, sizeof(T) > sizeof(void*)>::type t)
==> 命中EnableIf的EnableIf<T, true>的特化版本
==> f(A)

这里的enable_if是std::enable_if的简化版。

SFINAE是模版元编程最重要的理论基础,核心在于【构造一种模式,让其匹配成功或失败】

模版元编程

模版元就是静态计算+类型处理

静态计算

整数运算

template<int N>
struct Fib{
constexpr static int value = Fib<N - 2>::value + Fib<N - 1>::value;
};

template<>
struct Fib<0>{
constexpr static int value = 1;
};

template<>
struct Fib<1>{
constexpr static int value = 1;
};

int main(){
int a[Fib<10>::value];
}

逻辑运算

更多的是对类型进行判断

template<typename T1, typename T2>
struct is_same{
constexpr static bool value = false;
};

template<typename T>
struct is_same<T, T> {
constexpr static bool value = true;
};

template<typename T>
typename std::enable_if<is_same<T, int>::value, void>::type
f(T t){}

写起来比较啰嗦,所以更多是这样

template<typename T, T val>
struct integer_const{
constexpr static T value = val;
}

template<bool val>
using bool_constant = integer_constant<bool, val>;

类型处理(萃取)【type traits】

条件类型

std::conditional

template<bool cond, typename T1, typename T2>
struct conditional {
using type = T1;
};

template<typename T1, typename T2>
struct conditional<false, T1, T2>{
using type = T2;
};

template <typename T>
void f(typename conditional<std::is_fundamental<T>::value, T, const T&>::type t){

}

void Demo(){
int a = 0;
std::string str = "abc";
f<int>(a);
f<std::string>(str);
}

辅助工具

template <bool cond, typename T1, typename T2>
using conditional_t = typename conditional<cond, T1, T2>::type;

template <typename T>
void f(conditional_t<std::is_fundamental<T>::value, T, const T&> t){

}

案例

实现一个动态的get

// std::get返回值类型在编译期就是确定的
std::tuple<int, std::string, float> tp = {1, "1", 1.0};
std::cout << std::get<0>(tp);

// 如何实现一个动态的get, 比如get(tp, i)

//
// Created by ChenhuiWang on 2024/4/11.

// Copyright (c) 2024 Tencent. All rights reserved.
//
#include <tuple>
#include <string>
#include <iostream>

struct Base {
virtual void f() const = 0;
};

struct D1 : public Base {
void f() const override {
}
};

struct D2 : public Base {
void f() const override {
}
};

template<typename... Args>
std::enable_if_t<
std::conjunction_v<
std::is_base_of<Base, Args>...
>, void>
Invoke(const std::tuple<Args...> &tup, size_t index) {

}

int main() {
std::tuple tu{D1(), D2(), D1()};
Invoke(tu, 1);
}

然而Invoke函数里index是运行期变量,不能使用std::get<index>,所以采用编译期展开的方式

struct Base {
virtual void f() const = 0;
};

struct D1 : public Base {
void f() const override {
std::cout << "1\n";
}
};

struct D2 : public Base {
void f() const override {
std::cout << "2\n";
}
};


template<int N, typename... Args>
void TryInvoke(const std::tuple<Args...>& tup, size_t index){
if constexpr (N < sizeof...(Args)){
if(index == N){
std::get<N>(tup).f();
return;
}

TryInvoke<N + 1, Args...>(tup, index);
}
}

template<typename... Args>
std::enable_if_t<
std::conjunction_v<
std::is_base_of<Base, Args>...
>, void>
Invoke(const std::tuple<Args...> &tup, size_t index) {
if(index >= sizeof...(Args)){
return;
}
TryInvoke<0, Args...>(tup, index);
}

int main() {
std::tuple tu{D1(), D2(), D1()};
Invoke(tu, 0);
Invoke(tu, 1);
}

这里用到了模版实例化的递归,在编译期递归生成多个模版函数

实现一个std::invoke

template<typename T, typename... Args>
auto invoke(T&& obj, Args&&... args){
return obj(std::forward<Args>(args)...);
}

void f(int a){
std::cout << a;
}

int main(){
invoke(f, 4);
}

实现一个std::apply

template<typename T, typename... Args>
auto invoke(T&& obj, Args&&... args){
return obj(std::forward<Args>(args)...);
}

template<typename T, typename Tup, size_t... Index>
decltype(auto) my_apply(T&& obj, Tup&& tup){
return invoke(obj, std::get<Index>(std::forward<Tup>(tup))...);
}

void f(int a, int b){
std::cout << a << " " << b;
}

int main(){
invoke(f, 4, 5);
std::tuple<int, char>tup = {1, 'a'};
my_apply<void(*)(int, int), std::tuple<int, char>, 0, 1>(f, std::move(tup));
}

调用太啰嗦了,需要一个工具自动生成从0到N-1的序列

template<int N, int... Index>
struct A : public A<N - 1, N - 1, Index...> {

};

template<int... Index>
struct A<0, Index...> {
static void printIndex() {
((std::cout << Index << " "), ...);
}
};

int main() {
A<5>::printIndex();
}
template <size_t... Index>
struct sequence {
static void printIndex() {
((std::cout << Index << " "), ...) << std::endl;

}
};

template<size_t N, size_t... Index>
struct make_sequence : make_sequence<N - 1, N - 1, Index...> {};

template <size_t... Index>
struct make_sequence<0, Index...> {
using result = sequence<Index...>;
};

template<typename T, typename... Args>
auto invoke(T&& obj, Args&&... args) {
return obj(std::forward<Args>(args)...);
}

template<typename T, typename Tup, size_t... Index>
auto apply_impl(T&& obj, Tup&& tup, sequence<Index...>) {
return invoke(std::forward<T>(obj), std::get<Index>(std::forward<Tup>(tup))...);
}

template<typename T, typename Tup>
auto my_apply(T&& obj, Tup&& tup) {
make_sequence<std::tuple_size_v<std::decay_t<Tup>>>::result::printIndex();
typename make_sequence<std::tuple_size_v<std::decay_t<Tup>>>::result res;
return apply_impl(std::forward<T>(obj), std::forward<Tup>(tup), res);
}

void f(int a, double b, char c) {
std::cout << a << " " << b << " " << c ;
}

int main() {
std::tuple tu{1, 1.4, 'a'};
my_apply(f, tu);
return 0;
}

实现一个variant

std::variant用法

struct A{
A() = default;
};
int main(){
std::variant<A, int, char, std::string> var;
var = 1;
std::cout << var.index();
}

可以存放多种类型但同一时刻只能存放一种

template<typename T1, typename T2>
class variant{
public:
variant(const T1& t1){
data.t1 = t1;
};
variant(const T2& t2){
data.t2 = t2;
};

private:
union{
T1 t1;
T2 t2;
}data;
int index;
};

对于任意参数的variant无法映射到union中

本来variant的目的就是复用内存,因此我们可以自己管理一块内存

template<typename... Ts>
class variant{
public:
template<int Index, typename... Args>
variant(Args&&... args){

}
private:
void* data = std::malloc(std::max(sizeof(Ts)...));
int index;
};

int main(){
variant<1, int> va; //无法正确实例化模版类的模版构造函数
}

然而模版类的模版构造函数是不能实例化的,因为会被识别成类模版参数,因此构造函数只能依赖于自动推导

Index怎么推导呢 ==> 使用一个in_place模版类

template<size_t Index>
struct in_place_index_t{
explicit in_place_index_t() = default;
};

template<size_t Index>
inline constexpr in_place_index_t<Index> in_place_index{}


template<typename... Ts>
class variant{
public:
template<size_t Index, typename... Args>
variant(const in_place_index_t<Index>& index, Args&&... args){

}
private:
void* data = std::malloc(std::max(sizeof(Ts)...));
int index;
};

int main(){
variant<int, char> var_int{in_place_index<0>(), 1};
variant<int, char> var_char{in_place_index<1>(), 'a'};
}

实现构造函数需要,new 创建相应类型的对象,就需要从静态的模版参数size_t Index到具体类型的工具, 额外实现了一个get方法

template<size_t Index, typename Head, typename... Types>
struct get_type_by_index : get_type_by_index<Index - 1, Types...>{};

template<typename Head, typename... Types>
struct get_type_by_index<0, Head, Types...>{
using type = Head;
};

template<typename... Ts>
class variant{
public:
template<size_t Index, typename... Args>
variant(const in_place_index_t<Index>& index, Args&&... args){
using data_type = std::decay_t<typename get_type_by_index<Index, Ts...>::type>;
mData = new data_type(args...);
mIndex = Index;
}

int index() const {
return index;
}

template<size_t Index>
auto get() const {
using data_type = std::decay_t<typename get_type_by_index<Index, Ts...>::type>;
return *static_cast<data_type*>(mData);
}

private:
void* mData = std::malloc(std::max(sizeof(Ts)...));
int mIndex;
};

而析构、拷贝构造、拷贝赋值需要从运行时的mIndex成员变量到具体类型,采用静态穷举所有可能情况的方式

template<typename... Ts>
class variant {
private:
template<typename Type>
void destory_data() {
delete static_cast<std::add_pointer_t<Type>>(mData);
}

template<typename Type>
void create_data(const void* other_data){
new(mData) Type(*static_cast<const Type*>(other_data));
}

public:
~variant() {
std::array<void (variant<Ts...>::*)(), sizeof...(Ts)> destroy_functions = {
&variant<Ts...>::destory_data<Ts>...
};
if(mData != nullptr){
(this->*destroy_functions.at(mIndex))();
}
}

variant(const variant& other){
std::array<void(variant::*)(const void*), sizeof...(Ts)> create_functions = {
&variant::create_data<Ts>...
};
mIndex = other.mIndex;
(this->*create_functions.at(mIndex))(other.mData);
}

variant(variant&& other){
mData = other.mData;
mIndex = other.mIndex;
other.mData = nullptr;
other.mIndex = -1;
}

variant& operator=(const variant& other){
if(this == &other) return *this;
// release data
std::array<void (variant::*)(), sizeof...(Ts)> destroy_functions = {
&variant::destory_data<Ts>...
};
(this->*destroy_functions.at(mIndex))();

// copy assignment
std::array<void(variant::*)(const void*), sizeof...(Ts)> create_functions = {
&variant::create_data<Ts>...
};
mIndex = other.mIndex;
(this->*create_functions.at(mIndex))(other.mData);

return *this;
}

variant& operator=(variant&& other){
if(this == &other) return *this;

// release data
std::array<void (variant::*)(), sizeof...(Ts)> destroy_functions = {
&variant::destory_data<Ts>...
};
(this->*destroy_functions.at(mIndex))();

// move assignment
mData = other.mData;
mIndex = other.mIndex;
other.mData = nullptr;
other.mIndex = -1;

return *this;
}

}

然后想实现variant<int, char> var(1); 而不通过in_place_index来构造,进而可以支持implicit转化, variant<int, char> var = 1

需要一个工具从type ==> index

template <typename Target, typename Head, typename... Ts>
struct get_index_from_types {
constexpr static size_t value = get_index_from_types<Target, Ts...>::value + 1;
};

template <typename Target, typename... Ts>
struct get_index_from_types<Target, Target, Ts...>{
constexpr static size_t value = 0;
};

std::cout << get_index_from_types<Test1, int, char, std::string, Test1>::value;

template<typename T>
variant(T&& t){
constexpr size_t index = get_index_from_types<T, Ts...>::value;
new(mData) T(std::forward<T>(t));
mIndex = index;
}

template<typename Type>
auto get() const {
constexpr size_t index = get_index_from_types<Type, Ts...>::value;
return get<index>();
}

完整代码

//
// Created by ChenhuiWang on 2024/4/3.

// Copyright (c) 2024 Tencent. All rights reserved.
//
#include <iostream>
#include <array>


template<size_t Index>
struct in_place_index_t {
explicit in_place_index_t() = default;
};

template<size_t Index>
inline constexpr in_place_index_t<Index> in_place_index{};

template<size_t Index, typename Head, typename... Types>
struct get_type_by_index : get_type_by_index<Index - 1, Types...> {
};

template<typename Head, typename... Types>
struct get_type_by_index<0, Head, Types...> {
using type = Head;
};

template <typename Target, typename Head, typename... Ts>
struct get_index_from_types {
constexpr static size_t value = get_index_from_types<Target, Ts...>::value + 1;
};

template <typename Target, typename... Ts>
struct get_index_from_types<Target, Target, Ts...>{
constexpr static size_t value = 0;
};



template<typename... Ts>
class variant {
public:
template<size_t Index, typename... Args>
variant(const in_place_index_t<Index> &index, Args &&... args) {
using data_type = std::decay_t<typename get_type_by_index<Index, Ts...>::type>;
new(mData) data_type(args...);
mIndex = Index;
}

template<typename T>
variant(T&& t){
constexpr size_t index = get_index_from_types<T, Ts...>::value;
new(mData) T(std::forward<T>(t));
mIndex = index;
}

~variant() {
std::array<void (variant::*)(), sizeof...(Ts)> destroy_functions = {
&variant::destory_data<Ts>...
};
if(mData != nullptr){
(this->*destroy_functions.at(mIndex))();
}
}

variant(const variant& other){
std::array<void(variant::*)(const void*), sizeof...(Ts)> create_functions = {
&variant::create_data<Ts>...
};
mIndex = other.mIndex;
(this->*create_functions.at(mIndex))(other.mData);
}

variant(variant&& other){
mData = other.mData;
mIndex = other.mIndex;
other.mData = nullptr;
other.mIndex = -1;
}

variant& operator=(const variant& other){
if(this == &other) return *this;
// release data
std::array<void (variant::*)(), sizeof...(Ts)> destroy_functions = {
&variant::destory_data<Ts>...
};
(this->*destroy_functions.at(mIndex))();

// copy assignment
std::array<void(variant::*)(const void*), sizeof...(Ts)> create_functions = {
&variant::create_data<Ts>...
};
mIndex = other.mIndex;
(this->*create_functions.at(mIndex))(other.mData);

return *this;
}

variant& operator=(variant&& other){
if(this == &other) return *this;

// release data
std::array<void (variant::*)(), sizeof...(Ts)> destroy_functions = {
&variant::destory_data<Ts>...
};
(this->*destroy_functions.at(mIndex))();

// move assignment
mData = other.mData;
mIndex = other.mIndex;
other.mData = nullptr;
other.mIndex = -1;

return *this;
}

int index() const {
return index;
}

template<size_t Index>
auto get() const {
assert(Index == mIndex);
using data_type = std::decay_t<typename get_type_by_index<Index, Ts...>::type>;
return *static_cast<data_type *>(mData);
}

template<typename Type>
auto get() const {
constexpr size_t index = get_index_from_types<Type, Ts...>::value;
return get<index>();
}

private:
void *mData = std::malloc(std::max(sizeof(Ts)...));
int mIndex;

template<typename Type>
void destory_data() {
delete static_cast<Type*>(mData);
}


template<typename Type>
void create_data(const void* other_data){
new(mData) Type(*static_cast<const Type*>(other_data));
}
};


struct Test1{
~Test1(){
std::cout << "~Test1()" << std::endl;
}
};
struct Test2{
~Test2(){
std::cout << "~Test2()" << std::endl;
}
};

int main() {
variant<Test1, Test2> var1(in_place_index<0>);
// variant<Test1, Test2> var2(var1);
//
// std::variant<short, int> v = 1;
// std::cout << v.index();
// std::cout << get_index_from_types<int, int>::value;
//
// std::cout << get_index_from_types<Test1, int, char, std::string, Test1>::value;

struct A{
A(int m, char n) : m(m), n(n) {}
int m;
char n;
};

// std::variant<int, A> v(A{1, 'c'});
// v = A{2, 'a'};

variant<int, A> var3(A{1, 'c'});
var3 = A{2, 'a'};
std::cout << var3.get<A>().m;
std::cout << var3.get<0>();

std::variant<int, char> a(std::in_place_index<0>, 3);
}