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?,而這個能力或者意識是很多程序員缺失的,包括我。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章