用Rust的风格写C++

阅读量: searchstar 2023-02-15 14:37:44
Categories: Tags:

本文持续更新。

代码风格

函数返回值类型后置

C++11引入了函数返回值类型后置的写法(trailing return type):https://en.wikipedia.org/wiki/Trailing_return_type

auto func(int i, int j, int k) -> int {
	printf("2333");
	return 2333;
}

auto func(
	int i, int j, int k, int a, int b, int c, int d, int e, int f, int g, int h
) -> int {
	return 2333;
}

auto func(
	int i, int j, int k
) -> AVeryVeryLoooooooooooooooooooooooooooooooooooooooooooongType {
	return 2333;
}

auto func(
	int i, int j, int k
) -> std::tuple<
	Arg1,
	Arg2,
	...
	ArgN
> {
	return xxx;
}

构造器

ClassA(int a, int b, int c) : a_(a), b_(b), c_(c) {}
ClassA(
	int aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
	int bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
) : a_(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa),
	b_(bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb)
{
	// Constructor body
}

(不推荐).clang-format

# <https://clang.llvm.org/docs/ClangFormatStyleOptions.html>

AlwaysBreakTemplateDeclarations: Yes

AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
IndentWidth: 4
UseTab: Always
TabWidth: 4
IndentAccessModifiers: false
AccessModifierOffset: -4

AlignAfterOpenBracket: BlockIndent
ContinuationIndentWidth: 4
ConstructorInitializerIndentWidth: 4
...

AlignAfterOpenBracket: BlockIndent表示将圆括号里的东西按照block的风格来align:https://stackoverflow.com/questions/71710337/how-to-make-clang-format-not-align-parameters-to-function-call。但是它需要安装clang-format 14。Debian 11提供的clang-format的版本是11,可以用nix安装最新版的clang-format:nix-env -iA nixpkgs.clang-tools,nix包管理器教程:使用国内源安装和使用Nix包管理器

find . -regex '.*\.\(cpp\|cc\|cxx\|hpp\|h\)' -exec clang-format -style=file -i {} \;
# https://stackoverflow.com/questions/4210042/how-do-i-exclude-a-directory-when-using-find
# 如果要排除某目录的话
find . -regex '.*\.\(cpp\|cc\|cxx\|hpp\|h\)' -not -path "./子目录/*" -exec clang-format -style=file -i {} \;

存在的问题:

auto func(
	int i, int j, int k, int a, int b, int c, int d, int e, int f, int g, int h
)
	-> std::tuple<
		int, int, int, int, int, int, int, int, int, int, int, int, int, int> {
	return xxx;
}

因为目前BlockIndent只对圆括号起作用,对尖括号不起作用。

auto func(int i, int j, int k)
	-> AVeryVeryLoooooooooooooooooooooooooooooooooooooooooooongType {
	return 2333;
}

Rust -> C++

Box -> std::unique_ptr

Rc -> std::shared_ptr

Arc -> std::atomic<std::shared_ptr> (since C++20)

Ord -> std::weak_order, std::strong_order (since C++20)

注意C++中std::weak_ordering::equivalent不代表比较的双方可互相替换。而std::strong_ordering::equivalent代表比较的双方可以互相替换。

PartialOrd -> std::partial_order (since C++20)

Move-only object

一个典型的move-only class:

class A {
public:
	A(int v);
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	A(A&& a);
	A& operator=(A&& a);
};

例子:

#include <iostream>
using namespace std;
class A {
public:
	A(int v) : v_(v) {}
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	A(A&& a) : v_(a.v_) {}
	A& operator=(A&& a) {
		v_ = a.v_;
		return *this;
	}
	int V() const { return v_; }
private:
	int v_;
};
int main() {
	A a(1);
	std::cout << a.V() << std::endl;
	a = A(2);
	std::cout << a.V() << std::endl;
	return 0;
}

尤其要注意的是,C++中moved object仍然要析构,因此在move constructor和move assignment operator中一定要把旧的object相关状态清空,从而使其析构函数成为空操作。

可以用vscode clang-tidy静态检查是否使用了moved value:bugprone-use-after-move。教程:vscode clang-tidy

不使用构造函数

使用New等static member function来构造,而将构造函数隐藏起来:

#include <iostream>
class A {
public:
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	A(A&& a) { std::cout << "Moving\n"; }
	A& operator=(A&& a) { std::cout << "Move assigning\n"; return *this; }
	~A() { std::cout << "Deconstructing\n"; }
	static A New() { return A(); }
private:
	A() { std::cout << "Constructing\n"; }
};
int main() {
	A a = A::New();
	return 0;
}

输出:

Constructing
Deconstructing

可以看到只构造和析构了一次,实际上没有move assignment,所以跟使用构造函数的方法相比没有任何性能损失。

Option -> std::optional

基础用法

#include <iostream>
#include <optional>
auto func(bool cond) -> std::optional<std::string> {
	if (cond) {
		return "2333";
	} else {
		return std::nullopt;
	}
}
int main() {
	// 2333
	std::cout << func(true).value() << std::endl;
	// 0 (false)
	std::cout << func(false).has_value() << std::endl;
	return 0;
}

比较

#include <iostream>
#include <optional>
int main() {
	// 1
	std::cout << (std::optional<int>(2) == std::optional<int>(2)) << std::endl;
	// 1
	std::cout << (std::optional<int>(2) == 2) << std::endl;
	// 0
	std::cout << (std::optional<int>(2) == std::nullopt) << std::endl;
	return 0;
}

Optional reference

std::optional里是不能存reference的,因为C++ reference设计的时候就是对一个对象的alias,不能在container中使用。

std::reference_wrapper可以看作一个保证不为空的指针,因此可以结合std::optionalstd::reference_wrapper来实现optional reference,替代裸指针:

#include <iostream>
#include <string>
#include <optional>
auto choose(std::string& a, std::string& b, int index)
	-> std::optional<std::reference_wrapper<std::string>> {
	if (index == 0) {
		return std::ref(a);
	} else if (index == 1) {
		return std::ref(b);
	} else {
		return std::nullopt;
	}
}
int main() {
	std::string a("123");
	std::string b("456");
	// 123
	std::cout << choose(a, b, 0).value().get() << std::endl;
	// 456
	std::cout << choose(a, b, 1).value().get() << std::endl;
	// 0: false
	std::cout << choose(a, b, 2).has_value() << std::endl;
	return 0;
}

需要注意的是目前C++没有对std::optional<std::reference_wrapper<T>>进行优化,它比T *要多用一个字节来保存是否有值的信息:

#include <iostream>
#include <optional>
int main() {
	// 8
	std::cout << sizeof(char *) << std::endl;
	// 16。多出来的1个字节因为做了padding所以实际占用8字节。
	std::cout << sizeof(std::optional<std::reference_wrapper<char>>) << std::endl;
	return 0;
}

enum -> std::variant

match

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

template <typename Val, typename... Ts>
auto match(Val val, Ts... ts) {
	return std::visit(overloaded{ts...}, val);
}

用法:

match(value, [](Type1& type1) {
		;
	},
	[](Type2& type2) {
		;
	},
	...
	[](TypeN& typeN) {
		;
	}
);

例子:

#include <iostream>
#include <variant>
#include <string>

// https://en.cppreference.com/w/cpp/utility/variant/visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

template <typename Val, typename... Ts>
auto match(Val val, Ts... ts) {
	return std::visit(overloaded{ts...}, val);
}

class A {
public:
	A() : v_(0) {}
	A(int v) : v_(v) {
		std::cout << "Initializing " << V() << std::endl;
	}
	A(const A& a) : v_(a.v_) {
		std::cout << "Copying " << V() << std::endl; }
	A(A&& a) : v_(a.v_) {
		std::cout << "Moving " << V() << std::endl;
		a.Drop();
	}
	// Explicitly delete it just in case.
	A& operator=(const A&) = delete;
	A& operator=(A&& a) {
		v_ = a.v_;
		a.Drop();
		std::cout << "Move assigning " << V() << std::endl;
		return *this;
	}
	~A() {
		if (V()) {
			std::cout << "Deconstructing " << V() << std::endl;
		}
	}
	int V() { return v_; }
private:
	void Drop() {
		v_ = 0;
	}
	int v_;
};
auto func(bool choice) -> std::variant<A, int> {
	if (choice) {
		// Initializing 1
		return A(1);
		// Moving 1
	} else {
		return 233;
	}
};
auto main() -> int {
	A a;
	std::cout << match(func(true),
		[&](A& ret) -> std::string {
			// Move assigning 1
			a = std::move(ret);
			return "A";
		},
		[&](int ret) -> std::string {
			a = A(ret);
			return "int";
		}
	) << std::endl; // A
	// 1
	std::cout << a.V() << std::endl;

	return 0;
	// Deconstructing 1
}

输出:

Initializing 1
Moving 1
Move assigning 1
A
1
Deconstructing 1

如果类没有默认构造函数的话(比如std::reference_wrapper),按照Rust的写法,这个类应该通过返回值将其从visitor中传出来。但是C++要求所有visitor的返回值类型相同,因此这种方法行不通。比如这个:

#include <iostream>
#include <variant>

// https://en.cppreference.com/w/cpp/utility/variant/visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

template <typename Val, typename... Ts>
auto match(Val val, Ts... ts) {
	return std::visit(overloaded{ts...}, val);
}

int a = 233;
auto choose(bool err) -> std::variant<std::reference_wrapper<int>, int> {
	if (err) {
		return -1;
	} else {
		return std::ref(a);
	}
}

int main() {
	auto x = match(choose(false),
		[](std::reference_wrapper<int> x) {
			std::cout << x.get() << std::endl;
			return x;
		},
		[](int err) {
			std::cout << err << std::endl;
			throw err;
		}
	);

	return 0;
}

编译会报错:

std::visit requires the visitor to have the same return type for all alternatives of a variant

其实我们可以通过std::optional来在外面声明一个未初始化的变量,然后在visitor里面将其初始化:

#include <iostream>
#include <variant>
#include <optional>

// https://en.cppreference.com/w/cpp/utility/variant/visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

template <typename Val, typename... Ts>
auto match(Val val, Ts... ts) {
	return std::visit(overloaded{ts...}, val);
}

int a = 233;
auto choose(bool err) -> std::variant<std::reference_wrapper<int>, int> {
	if (err) {
		return -1;
	} else {
		return std::ref(a);
	}
}

int main() {
	std::optional<std::reference_wrapper<int>> x;
	match(choose(false),
		[&x](std::reference_wrapper<int> ret) {
			std::cout << ret.get() << std::endl;
			x = ret;
		},
		[](int err) {
			std::cout << err << std::endl;
		}
	);
	if (x.has_value()) {
		std::cout << x.value() << std::endl;
	}

	return 0;
}

std::unique_ptr也是可以的,缺点是需要多一层indirection。

参考:

https://en.cppreference.com/w/cpp/utility/variant/visit

https://polomack.eu/std-variant/

https://www.cppstories.com/2019/10/lazyinit/

std::variant::index

可以用index()获取其类型的下标,用std::get获取对应的内容:

#include <iostream>
#include <variant>
int main() {
	std::variant<int, float> a(2);
	// 0 (int)
	std::cout << a.index() << std::endl;
	// 2
	std::cout << std::get<0>(a) << std::endl;

	std::variant<int, float> b((float)2.33);
	// 1 (float)
	std::cout << b.index() << std::endl;
	// 2.33
	std::cout << std::get<1>(b) << std::endl;
	return 0;
}

Trait

Rust可以为已存在的类型实现trait,从而为其引进新的成员函数,但是C++不能为已存在的类型定义新的成员函数。不过C++可以通过以下方式实现Rust的trait的功能:

(不推荐)template struct

#include <iostream>
struct A {
	A(int x) : x_(x) {}
	A(const A&) {
		std::cout << "Copying" << std::endl;
	}
	int x_;
};
template <typename T>
struct Print {
	static void print_type();
	static void print(T x) {
		std::cout << x << std::endl;
	}
};
template <>
struct Print<int> {
	static void print_type() {
		std::cout << "int" << std::endl;
	}
	static void print(int x) {
		std::cout << x << std::endl;
	}
};
template <>
struct Print<const A&> {
	static void print_type() {
		std::cout << "const A&" << std::endl;
	}
	static void print(const A& a) {
		std::cout << a.x_ << std::endl;
	}
};
int main() {
	// int
	Print<int>::print_type();
	// 233
	Print<int>::print(233);
	// const A&
	Print<const A&>::print_type();
	// 2333. Without copying
	Print<const A&>::print(A(2333));
	return 0;
}

它相当于:

struct A {
    x: i32,
}
impl A {
    fn new(x: i32) -> A {
        A { x }
    }
}
trait Print {
    fn print_type();
    fn print(self);
}
impl Print for i32 {
    fn print_type() {
        println!("i32");
    }
    fn print(self) {
        println!("{}", self);
    }
}
impl Print for &A {
    fn print_type() {
        println!("&A");
    }
    fn print(self) {
        println!("{}", self.x);
    }
}
fn main() {
	// i32
    i32::print_type();
    let x: i32 = 233;
	// 233
    x.print();
    // &A
    <&A>::print_type();
    // 2333
    A::new(2333).print();
}

但是这种方法要求所有的template class的specialization都在同一个namespace里。这可能会导致链接冲突,比方说在两个不同的obj文件分别specialize了Print<const A&>

CPO + tag_invoke

CPO: Customization Point Object.

详细的讲解:c++ execution 与 coroutine (一) : CPO与tag_invoke

这里简单归纳一下。

背景

ADL (Argument Dependent Lookup)是指,如果有限定地调用某个函数时,比如lib::print,那么只会在限定的namespace里查询该函数。如果在没有限定(unqualified)地调用某个函数时,比方说print(x, y),那么编译器不仅会在调用点的namespace里查询有没有叫做print的函数,也会在x和y的namespace里查询有没有叫做print的函数。如果x和y是实例化的模板的话,还会从模板参数所在的namespace里查询有没有叫做print的函数。

例子:

#include <iostream>
#include <vector>
namespace lib {
template <typename T>
void print(T x) {
	std::cout << x;
}
template <typename T>
void print(const std::vector<T>& v) {
	for (const auto& x : v) {
		print(x);
	}
}
}
struct A {int x;};
std::ostream& operator<<(std::ostream& out, const A& a) {
	out << a.x;
	return out;
}
void print(const A& a) {
	std::cout << "{x:" << a.x << "}";
}
int main() {
	std::vector<A> v({A{2}, A{3}, A{4}});
	lib::print(v);
	return 0;
}

输出:{x:2}{x:3}{x:4}lib::print只会去lib中找print,显然找到的是void print(const std::vector<T>& v)。然后在里面调用print(x)时,因为x的类型是const A&,所以会去A所在的namespace里找print,显然找到的是void print(const A& a),然后就顺利打印出来了。

但是如果我们在main函数里这样:

lib::print(A{2});

那么C++只会去lib里找print,找到的就是void print(T x)了。因此打印出来的就是2,而不是{x:2}。但是显然这时调用void print(const A& a)是更合理的选择。

CPO (Customization Point Object)

CPO的目标是将原本的有限定的函数调用变成无限定的函数调用,而库只提供调用的接口。比如用CPO来实现lib::print

#include <iostream>
#include <vector>
namespace lib {
namespace detail {
struct print_t {
	template <typename T>
	void operator()(T x) {
		print(x);
	}
};
template <typename T>
void print(T x) {
	std::cout << x;
}
template <typename T>
void print(const std::vector<T>& v) {
	for (const auto& x : v) {
		print(x);
	}
}
} // namespace detail
inline detail::print_t print{};
} // namespace lib
struct A {int x;};
std::ostream& operator<<(std::ostream& out, const A& a) {
	out << a.x;
	return out;
}
void print(const A& a) {
	std::cout << "{x:" << a.x << "}";
}
int main() {
	lib::print(A{2});
	return 0;
}

这时lib::print变成了一个对象(object),lib::print(A{2})变成了调用lib::detail::print_t::(),在里面调用了print(x),它是无限定的,所以既会在调用点的namespace(即lib::detail)里找print,也会在其参数的类型(即const A&)的namespace里找,所以顺利找到了void print(const A& a),并且打印出来了{x:2}

可以看到,对于纯CPO的方式,如果某个类想要实现lib提供的print的接口,那么必须在其namespace中预留print的名字。这就会带来一个问题,假如另一个库lib2也提供了print接口,而某个类想要同时实现lib::printlib2::print的接口,就会导致命名冲突。

tag_invoke + CPO

tag_invoke可以解决纯CPO的命名冲突,思路是让所有接口都通过tag_invoke实现,而调用时通过tag来指定具体要调用哪个实现。上面的例子用tag_invoke + CPO重写:

#include <iostream>
#include <vector>
namespace lib {
namespace detail {
struct print_t {
	template <typename T>
	void operator()(T x) {
		tag_invoke(print_t{}, x);
	}
};
template <typename T>
void tag_invoke(print_t, T x) {
	std::cout << x;
}
template <typename T>
void tag_invoke(print_t, const std::vector<T>& v) {
	for (const auto& x : v) {
		tag_invoke(print_t{}, x);
	}
}
} // namespace detail
template <auto& Tag>
using tag_t = std::decay_t<decltype(Tag)>;
inline detail::print_t print{};
} // namespace lib
struct A {int x;};
std::ostream& operator<<(std::ostream& out, const A& a) {
	out << a.x;
	return out;
}
void tag_invoke(lib::tag_t<lib::print>, const A& a) {
	std::cout << "{x:" << a.x << "}";
}
int main() {
	std::vector<A> v({A{2}, A{3}, A{4}});
	lib::print(v);
	std::cout << std::endl;
	lib::print(A{2});
	return 0;
}

可以看到,这里的lib::detail::print_t成了tag,用于区分tag_invoke的各个实现。这个tag是在detail里的,所以另外提供了std::tag_t用来从接口(也就是lib::print)取得tag的类型。

输出:

{x:2}{x:3}{x:4}
{x:2}

相关:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2279r0.html

tag_invoke + type tag

如何利用tag_invoke实现反序列化(deserialize)呢?理想的tag应该是这样的:

struct deserialize_t {
	template <typename T>
	T operator()(std::string_view x) {
		return tag_invoke(deserialize_t{}, x);
	}
};

但是C++目前无法根据返回值来推断T的类型,例如下面这段代码无法通过编译:

#include <iostream>
using namespace std;
struct A {
	A() { std::cout << 233; }
};
struct B {
	template <typename T>
	T operator()() {
		return T();
	}
};
int main() {
	B()();
	return 0;
}
template_operator.cpp: In function ‘int main()’:
template_operator.cpp:16:12: error: no match for call to ‘(B) ()’
   16 |         B()();
      |         ~~~^~
template_operator.cpp:10:11: note: candidate: ‘template<class T> T B::operator()()’
   10 |         T operator()() {
      |           ^~~~~~~~
template_operator.cpp:10:11: note:   template argument deduction/substitution failed:
template_operator.cpp:16:12: note:   couldn’t deduce template parameter ‘T’
   16 |         B()();
      |         ~~~^~

要指定要调用的operator()的类型,只能这样:

B().operator()<A>();

这十分麻烦。因此这里采用type tag的方式,即定义一个空的模板结构体template <typename T> struct type_tag {};,它的功能仅仅是作为类型T的tag,用来帮助编译器找到对应的模板函数。结合tag_invoke和type tag实现的反序列化如下:

#include <iostream>
#include <vector>

namespace lib {
template <typename T>
struct type_tag_t {};
template <typename T>
inline constexpr type_tag_t<T> type_tag{};

namespace detail {
class deserialize_t;
template <typename T>
T tag_invoke(deserialize_t, type_tag_t<T>, std::string_view x);
struct deserialize_t {
	template <typename T>
	T operator()(type_tag_t<T>, std::string_view x) {
		return tag_invoke(deserialize_t{}, type_tag<T>, x);
	}
};
int tag_invoke(deserialize_t, type_tag_t<int>, std::string_view x) {
	return *(int *)x.data();
}
} // namespace detail
template <auto& Tag>
using tag_t = std::decay_t<decltype(Tag)>;
inline detail::deserialize_t deserialize{};
} // namespace lib

struct A {int x;};
A tag_invoke(lib::tag_t<lib::deserialize>, lib::type_tag_t<A>, std::string_view x) {
	return *(A *)x.data();
}

int main() {
	int x = 2333;
	std::string_view d((const char *)&x, sizeof(x));
	std::cout << lib::deserialize(lib::type_tag<int>, d) << std::endl;

	A a{233};
	d = std::string_view((const char *)&a, sizeof(a));
	std::cout << lib::deserialize(lib::type_tag<A>, d).x << std::endl;

	return 0;
}

注意,由于ADL的限制,对基础数据类型(如float)和第三方类型(即不属于调用点的namespace),无法用这种方法定义其反序列化操作。Rust也采用了这种方式,即trait的implementation要么在trait声明的地方,要么在类型定义的地方。