C++ & Rust Type-Traits学习笔记
C++ & Rust Type/Traits学习笔记
前言
打算对着Rust的Traits学习一下C++的Type-Traits,因为Rust的Traits相对比较简单,我原来以为Rust的Traits就是C++的Type-Traits,但是发现Rust的Traits和C++的Type-Traits还是有很大的区别的,所以既然来都来了,不如一起看一下,学习学习。先学习一些基础的,等后面再学习一下更加复杂的。现在的内容还没有击中重点,比如内存模型,生命周期和所有权等等,这些内容后面再学习。
以下内容全部居于网络和本人对于语言特性并不深刻的理解,如果有错误的地方,欢迎指正。
为什么不先看模板泛型?
根据我的理解,在C++里,模板泛型是实现编译期计算和多态的一种重要手段,但是Rust这个语言则是直接把编译期计算的能力内置到了语言里面,编译器在编译Rust代码的时候,逻辑本来就是收紧的。比如很多C++等其他高级语言可以填var类型的地方,但是在Rust里面必须是literal类型,这在很大一个程度上,保障了Rust优化的空间和编译期计算的能力。
还有一些其他的特性则或多或少继承了Ocaml的特征,所以这块内容回来还是得好好看一下的。
模板泛型暂时不影响我们对type-traits的理解,所以先不看模板泛型。
Rust Traits
Rust里面的面对对象编程的概念其实和C++挺不一样的,我个人觉得Rust的面对对象编程更像是Go的面对对象编程,而C++的面对对象编程更像是Java的面对对象编程。Rust的trait(这里不不包括advanced trait)更像是一个实现继承的手段,不过其并不强调继承,而是着重实现一个类和其下属类的统一的规范。
在cpp的面向对象编程里,我们可以通过继承和多态来实现代码的复用和对代码的抽象,比如说我们有一个Food类,那么我们可以通过继承来实现一个Apple类,一个Banana类,这样我们就可以通过Food类来抽象出Apple和Banana这两个类,这样我们就可以通过Food类来实现对Apple和Banana的操作,比如说我们可以通过Food类来实现对Apple和Banana等同属于food类的行为进行定义。
上述例子在C++里可以被这样实现,假设我们有一个food类
#include <bits/stdc++.h>
#include <utility>
using namespace std;
class food{
private:
string name;
public:
food(string && name, int price): name(std::move(name)), price(price){}
int price;
virtual void smell(){
cout << "food smell" << endl;
};
virtual void taste(){
cout << "food taste" << endl;
};
void weight(){
cout << "food weight" << endl;
};
};
class Apple: public food{
public:
Apple(string name, int price): food(std::move(name), price){}
virtual void smell(){
cout << "apple smell" << endl;
};
virtual void taste(){
cout << "apple taste" << endl;
};
};
int main() {
food * f = new Apple("apple", 10);
f->smell(); // apple smell
f->taste(); // apple taste
f->weight(); // food weight
return 0;
}
上述代码中,我们通过继承来实现了Apple类,Apple类继承了food类,这样我们就可以通过food类来抽象出Apple类,这样我们就可以通过food类来实现对Apple类的操作,比如说我们可以通过food类来实现对Apple类等同属于food类的行为进行定义。并且如果子类没有实现父类的某个方法,那么父类的方法就会被调用。
相似的,在Rust里,我们也可以通过trait来实现代码的复用和对代码的抽象。
use std::io::{BufRead,stdin};
use std::cmp::max;
use std::f32::INFINITY;
trait Food {
fn taste(&mut self) -> String{
let mut s = String::from("food taste");
s
}
fn weight(&mut self) -> String{
let mut s = String::from("food weight");
s
}
fn smell(&mut self) -> String{
let mut s = String::from("food smell");
s
}
}
struct Apple {
name: String,
}
impl Food for Apple{
fn taste(&mut self) -> String{
let mut s = String::from("apple taste");
s
}
fn weight(&mut self) -> String{
let mut s = String::from("apple weight");
s
}
}
fn main() {
let mut b = Apple{name: String::from("apple")};
println!("{}",b.taste()); // apple taste
println!("{}",b.weight()); // apple weight
println!("{}",b.smell()); // food smell
type BoxedFood = Box<dyn Food>;
let mut a: BoxedFood = Box::new(Apple{name: String::from("apple")});
println!("{}",a.taste()); // apple taste
}
在直接的父子关系继承里面,我们可以用Rust实现等价的代码,但是回到我们刚刚说的,Rust和Go的继承更加相像,Go的interface不可以被继承,同样,不能出现多重继承,所以Rust的trait也不可以被继承,也不能出现多重继承。
另外,一个trait可以被多个类实现,一个类可以实现多个trait,但rust语法不支持一个impl块实现多个trait,我们要分开来写。以后我们还会用到更多的trait相关内容。
注意事项
一个trait的作用域在当前module是全局的,所以我们在使用trait的时候,需要注意trait的命名,避免和其他的trait重名。
同时,这个trait的生命周期仅在当前module中,如果我们想要在其他crate中使用这个trait,那么我们需要在trait前面加上pub关键字,这样这个trait就会被导出到外部。这样就保证了一些trait的安全性。
内置容器的实现可能用到了一些trait,如果我们可以随意覆写这些trait,那么我们就可以实现一些不安全的操作,这并不是很理想。
C++ Type Traits
首先需要回顾一下C++的编译期计算,也就是我们常说的模板泛型,我们可以通过模板泛型来实现编译期计算,利用模板和特化,我们可以实现Ocaml和prolog等语言中,通过逻辑推理来实现特定功能的能力。当然,Ocaml中的推理过于纯粹,限制较多,cpp作为一个imperative language,则需要兼容高级语言和函数式语言的特性,所以cpp必须有自己的编译期工具,否则编译器无法分清什么时候该是编译期的,什么时候该是运行期的,所以我们就有了type traits。
获取类型信息
假如说我们现在需要有一个constexpr工具来判断某个传入的type是否是一个自定义的class叫BigInt,我们可以这样实现
#include <bits/stdc++.h>
#include <utility>
using namespace std;
struct bigint{
// some code
};
template <typename T>
struct node{
constexpr static bool value = false;
};
template <>
struct node<bigint>{
constexpr static bool value = true;
};
int main() {
if constexpr (node<bigint>::value) {
cout << "bigint" << endl; // this is printed
} else {
cout << "not bigint" << endl;
}
if constexpr (node<int>::value) {
cout << "int" << endl;
} else {
cout << "not bigint" << endl; // this is printed
}
return 0;
}
这样我们就实现了node的一个type_traits,可以在编译期间判断某个type是否是bigint。
我们的C++标准库里,已经为我们提供了一些type_traits,比如说std::is_same,这个函数可以在编译期间判断两个type是否相同,我们可以这样实现。
其实,我们可以用更简单的方法来实现这个功能,就是用std::is_same,这个函数可以在编译期间判断两个type是否相同,我们可以这样实现
#include <bits/stdc++.h>
#include <utility>
using namespace std;
struct bigint{
// some code
};
template <typename T>
struct node{
constexpr static bool value = std::is_same<T, bigint>::value;
};
还有一个更高级的用法,来自zero大佬的博客,通过is_invocable来判断某个函数是否可以被调用,我们可以在C++里面实现简单的higher-order function, 也就是柯里化
template<typename F>
auto curry(F&& f) {
if constexpr (std::is_invocable_v<F>) {
return f();
} else {
return [=]<typename T>(T&& x) {
return curry(
[=]<typename... Ts>(Ts&&... xs)->std::invoke_result_t<F, T, Ts...> {
return std::invoke(f, x, xs...);
}
);
};
}
}
int main() {
auto f = [](int a, int b, int c){ return a + b + c; };
std::cout << curry(f)(1)(2)(3) << std::endl; // output: 6
}
实现宏定义的替换
我们可以利用type traits来实现宏定义的替换,比如我们在算法竞赛中常用的INF是0x3f3f3f3f(虽然我知道用这个宏并不只是因为方便),但是对于模板元编程,INT_MAX一个是不好记,另一个是可能污染空间,所以type_traits可以帮助我们实现这个功能。
比如
numeric_limits<int>::max(); // 2147483647
numeric_limits<int>::min(); // -2147483648
numeric_limits<int>::lowest(); // -2147483648
numeric_limits<int>::epsilon(); // 1
numeric_limits<int>::round_error(); // 0
numeric_limits<double>::epsilon(); // 2.22045e-16
在一些不会+=的地方,我们可以用numeric_limits来实现INF的优雅替换。
实现类型转换
type_traits还可以帮我们实现一些简单的类型萃取和转换。
Recall:
在linux内部提供的链表当中,我们可以通过container_of宏来实现从一个field指针到整个struct指针的替换,那么在C++中,我们可以用type_traits来实现这个功能。
int main() {
struct pp{
int a;
int b;
};
pp * n;
remove_pointer<decltype(n)>::type m;
if constexpr(is_same<decltype(m), pp>::value){
cout << "m is a pp type" << endl; // this is printed
}else{
cout << "m is a pp ptr type" << endl;
}
return 0;
}
这就神似ocaml中的type interence的实现。
那么在C++中,我们可以用type_traits来实现这个功能。
template <typename T>
struct remove_pointer {
typedef T type;
};
template <typename T>
struct remove_pointer<T*> {
typedef T type;
};
template <typename T>
struct remove_pointer<T* const> {
typedef T type;
};
template <typename T>
struct remove_pointer<T* volatile> {
typedef T type;
};
template <typename T>
struct remove_pointer<T* const volatile> {
typedef T type;
};
这就令人不得不联想到ocaml中的type interence和tuple值得提取的实现。
比如我现在有一个元组tp(name, age), 那么我想要获取name的类型,我可以这样实现
let tp = ("name", 18)
match tp with
| (name, age) -> name (* return 18 *)
| _ -> raise Not_found
留一个小小的坑,看到rust泛型的时候再填,rust的泛型实现是不是也是用的type traits呢?或者至少也有类似的实现吧。让我们拭目以待