與視頻內容重複
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<>
,struct
,std::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存儲類型信息。
而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 spaces和types不匹配的地方。
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 space
與types
不匹配的例子。例如,處於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::variant
和std::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?,而這個能力或者意識是很多程序員缺失的,包括我。