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呢?或者至少也有类似的实现吧。让我们拭目以待

posted @ 2023-03-18 11:20  tiany7  阅读(72)  评论(0编辑  收藏  举报