CppCon 2016: Ben Deane “Using Types Effectively" 笔记

与视频内容重复

std::optional && std::variant

optional

std::optional是由A proposal to add a utility class to represent optional objects提出来的?里面详细介绍了std::optional的设计以及背后的原因。

cppreference里面详细介绍了std::optional,《C++17 The Complete Guide》有详细的介绍,以后我再学习,这里粘贴一个其中的例子。

#include <optional>
#include <string>
#include <iostream>

// convert string to int if possible
std::optional<int> asInt(const std::string& s) {
	try {
		return std::stoi(s);
	} catch(...) {
		return std::nullopt;
	}
}

int main() {
	for (auto s : {"42", "  077", "hello", "0x33"}) {
		// try to convert s to int and print the result if possible
		std::optional<int> oi = asInt(s);
		if (oi) {
			std::cout << "convert '" << s << "' to int: " << *oi << "\n";
		} else {
			std::cout << "can't convert '" << s << "' to int\n";
		}
	}
}

std::optional的出现是为了描述一些可能存在值或不存在的情况,例如英文名中的middle name,以前可以用std::pair<string, bool>来表示,但

  • string值和bool值是有重叠的,string值本身就表示了true
  • 可能存在string值和false并存的情况,也就是type层面就会容忍这种bug的出现,可能需要添加一些测试来检测这种情况

归根到底就是type层面不能精确表达存在值或者什么都没有的状态语义,只能通过其它形式去模拟。也有人通过std::unique_ptr来模拟这种语义,但是开销比较大,而且不是值语义。

std::variant

The class template std::variant represents a type-safe union.

std::variant是由Variant: a type-safe union提出来的,里面介绍了std::variant有关的详细细节。例如

union versus variant
This proposal is not meant to replace union: its undefined behavior when casting
Apples to Oranges is an often used feature that distinguishes it from variant’s
features. So be it.
.
On the other hand, variant is able to store values with non-trivial constructors
and destructors. Part of its visible state is the type of the value it holds at a
given moment; it enforces value access happening only to that type.

如下面例子所示,下面的代码会抛出EXCEPTION: bad_variant_access,然后被后面的catch语句捕获,这也是为什么说std::variant能够记录值的类型信息,而在std::variant的实现中也是这样做的。你可以用std::variant::index()来获得当前值的类型。

#include <varianr>
#include <iostream>

int main() {
	std::variant<int, std::string> var{"hi"};
	std::cout << var.index() << '\n';
	try{
		int i = std::get<0>(var); // EXCEPTION: bad_variant_access
	} catch (const std::bad_variant_access& e) {
		std::cerr << "EXCEPTION: " << e.what() << '\n';
	}
}

Product Type

PLP好像并没有介绍product type或者相关的信息,TAPL介绍到了,但是我还没有读。这里摘抄wiki和《#23 Product Types》中的内容

In programming languages and type theory, a product of types is another, compounded, type in a structure. The “operands” of the product are types, and the structure of a product type is determined by the fixed order of the operands in the product. - 《Product type

  • product types主要用来组织逻辑上相关的数据
  • product types将多种不同的types合成一个,例如real * string

像C++中的std::pair<>structstd::tuple或者其它语言中类似的类型。而这些类型有的需要通过index来获取子数据,有的需要id(比如说field name)。

而在视频《Using Types Effectively》中,Ben和大家玩了一个关于type的游戏,可以直观表达product type所能表达的语义。例如下面的一系列的类型:

// 有256 * 2个值
struct Foo {
	char a;
	bool b;
}

// 有2 * 2 * 2个值
std::tuple<bool, bool, bool>;

// 有(# of values in T)* (# of values in U)
template<typename T, typename U>
struct Foo {
	T m_t;
	U m_u;
}

Ben这个游戏的目的就是告诉大家代码中的一些类型,例如struct能够表达出的值的数量,即使==有些时候这些值的数量远超我们原本想要表达的值的数量==。

Sum Type

In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use. - 《Tagged union》

根据定义,C语言中的union是残缺的,不是type-safe的,因为它没有所谓的tag field来表征值的类型,需要程序员以external的方式牢记union可能的类型。为人所熟知的pattern matching就是用于sum type上的。而正确的实现方式,应该像下图一样,有一个额外的field存储类型信息。sum types
而C++ 17中的std::variant正式提供了这样一种type-safe的sum type,Ben的另外几个代码示例:

// 有 (# of values in T) + (# of values in U),注意这里不是乘法而是加法
template <typename T, typename U>
struct Foo {
	std::variant<T, U>;
}

Ben想告诉大家的是,其实很多明明可以用sum type来表达的值,却使用了product type来表达,无形中增加了很多潜在的bug和无用的测试代码。想想我自己的代码中也存在很多这种state spacestypes不匹配的地方。

We have a choice over how to represent values. std::variant will quickly become a very important tool for proper expression of states.

例如下面的用来表示server连接状态的代码:

enum class ConnectionState {
	DISCONNECTED,
	CONNECTING,
	CONNECTED,
	CONNECTION_INTERRUPTED
};

struct Connect {
	ConnectionState m_connectionState;

	std::string m_serverAddress;
	ConnectionId m_id;
	std::chrono::system_clock::time_point m_connectedTime;
	std::chrono::milliseconds m_lastPingTime;
	Timer m_reconnectTimer;
}

上面的代码就是一个state spacetypes不匹配的例子。例如,处于DISCONNECTED状态下,没有所谓的m_connectedTime等值的,对于这样的代码,你可能需要测试在DISCONNECTED状态下,m_connectedTime这些值应该处于无效状态。正确的做法应该是选择正确的type,在编写代码的过程中彻底杜绝(不需要程序员参与,type就不允许这些非法状态的存在)这些状态的存在。

struct Connection {
	std::string m_serverAddress;

	struct Disconnected {};
	struct Connecting {};
	struct Connected {
		ConnectionId m_id;
		std::chrono::system_clock::time_point m_connectedTime;
		std::optional<std::chrono::milliseconds> m_lastPingTime;
	};
	struct ConnectionInterrupted {
		std::chrono::system_clock::time_point m_disconnectedTime;
		Timer m_reconnectedTimer;
	};
	
	std::variant<Disconnected,
				Connecting,
				Connected,
				ConnectionInterrupted> m_connection;
};

我们可以从上述代码中看到Ben使用std::variantstd::optional精确地表达了Connection应该有的状态,杜绝了无效状态的存在。首先从最顶层来说,Connection值的状态空间就应该是server address * connection state。而connection state应该是choice type也就是sum type

Using types to constrain behavior

这是《Using Types Effectively》中的一个章节,这也是Ben想要表达的核心。后面Ben还玩儿了一个“Name that function”的游戏,这个游戏的目的是为了想要让大家知道函数就应该做它应该做的事,不要返回意料之外的值,感兴趣的可以去看原视频。

粘贴一些原视频的函数例子:

template <typename T>
T f(vector<T>);

其实这样的函数就不应该存在,因为vector可能是空的,此时不可能返回一个T出来(抛开创建新T值的情况),但是标准库中就存在这样的函数,例如std::vector::front,这就是所谓由于type设计的问题,存在触发undefined behavior的可能性。

// Calling front on an empty container is undefined.
T& vector<T>::front();

而合理的设计是下面的这种形式。

template <typename T>
optional<T> f(vector<T>);

类似于这样的例子还有很多,视频中还有很多很多,这个视频牛逼的地方不在于给你一个规范,什么时候使用std::variant,而是通过平铺直叙的方式给程序员以想法上的改变,如何设计type?,而这个能力或者意识是很多程序员缺失的,包括我。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值