与视频内容重复
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?,而这个能力或者意识是很多程序员缺失的,包括我。