You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: lectures/optional.md
+52-32Lines changed: 52 additions & 32 deletions
Original file line number
Diff line number
Diff line change
@@ -11,17 +11,18 @@
11
11
-[What is error handling after all](#what-is-error-handling-after-all)
12
12
-[What to do about unrecoverable errors](#what-to-do-about-unrecoverable-errors)
13
13
-[How to recover from recoverable errors](#how-to-recover-from-recoverable-errors)
14
+
-[Why not set a global value](#why-not-set-a-global-value)
14
15
-[Why not throw an exception](#why-not-throw-an-exception)
15
-
-[Why I don't use exceptions much](#why-i-dont-use-exceptions-much)
16
-
-[Avoid the hidden error path](#avoid-the-hidden-error-path)
16
+
-[Exceptions are expensive](#exceptions-are-expensive)
17
+
-[The hidden path is hidden](#the-hidden-path-is-hidden)
18
+
-[Use return type for explicit error path](#use-return-type-for-explicit-error-path)
17
19
-[How to work with `std::optional`](#how-to-work-with-stdoptional)
18
20
-[Use `std::expected` to tell why a function failed](#use-stdexpected-to-tell-why-a-function-failed)
19
21
-[Use `std::optional` to represent optional class fields](#use-stdoptional-to-represent-optional-class-fields)
20
22
-[How are they implemented and their performance implications](#how-are-they-implemented-and-their-performance-implications)
21
23
-[Summary](#summary)
22
24
23
-
When writing code in C++, just like in life overall, we don't always get what we want. The good news is that we can prepare by being careful and anticipating some of the errors that we can encounter. There are many mechanisms in C++ for this and today we're talking about what options we have with an added benefit of some highly opinionated suggestions. All of you experienced C++ devs, prepare your pitch forks! :wink:
24
-
25
+
When writing code in C++, just like in life overall, we don't always get what we want. The good news is that we can prepare by being careful and anticipating some of the errors that we can encounter. Just like with everything else in C++, there are many mechanisms for this and today we're talking about what options we have with an added "benefit" of some highly opinionated suggestions. All of you experienced C++ devs, prepare your pitch forks! :wink:
25
26
26
27
<!-- Intro -->
27
28
@@ -33,20 +34,22 @@ I aim to add links to opinions alternative to those expressed in this lecture to
33
34
34
35
## What is error handling after all
35
36
36
-
It makes sense to start our conversation with defining what we call an "error" in the first place in the context of our C++ code. Essentially, on the highest level of abstraction, we say that there was an error when the code does not produce the result we expect it to produce.
37
+
With the disclaimer out of the way, it makes sense to start our conversation by defining what we call an "error" in the first place in the context of our C++ code.
38
+
39
+
Essentially, on the highest level of abstraction, we say that there was an error when the code does not produce the result we expect it to produce.
37
40
38
-
We can further classify the possible error by its origin. The errors are typically thought of as:
41
+
We can further classify the possible errors by their origin. The errors are typically thought of as:
39
42
40
-
- recoverable: errors that we can recover from within the normal operation of the program. An example of these would be a network timeout.
41
-
- unrecoverable: errors that indicate a state of the program so broken that any recovery is a moot point. Typical examples are programmatic errors and errors resulting from undefined behavior encountered previously in the program.
43
+
-**recoverable:** errors that we can recover from within the normal operation of the program. An example of these would be a network timeout in a situation when a user can wait and retry.
44
+
-**unrecoverable:** errors that indicate a state of the program so broken that any recovery is useless. Typical examples are programmatic errors and errors resulting from undefined behavior encountered previously in the program.
42
45
43
46
<!-- Link the CppCon talk by Andreas, maybe also Aleksandrescu? -->
44
47
45
-
Note thatthis classification is still highly debated. There is a large camp of people, who believe that every error is potentially recoverable and should be treated as such. This is a valid way of thinking but it comes with a price that, at least in my industry, people are usually unwilling to pay.
48
+
Note that, while some languages, like Rust, make this distinction directly in their official documentation, the classification of errors into recoverable and unrecoverable is still highly debated in C++. There is a large camp of people, who believe that every error is potentially recoverable and should be treated as such and that an error should be reported for potentially being handled later at a different place in the program. This is absolutely a valid way of thinking but it comes with a price that, at least in my industry, people are usually unwilling to pay.
46
49
47
50
## What to do about unrecoverable errors
48
51
49
-
Here, we will assume that we cannot or don't want to try to recover from a class of errors that we deem "unrecoverable". That being said, we still generally want to have tools to reduce the likelihood of these errors popping up. In my experience, most of these errors come from an erroneous assumption or an undetected error earlier in the program.
52
+
In this lecture, we will assume that we cannot or don't want to try to recover from a class of errors that we deem "unrecoverable". That being said, we still generally want to have tools to reduce the likelihood of these errors popping up. In my experience, most of these errors come from an erroneous assumption or an undetected error earlier in the program.
50
53
51
54
One typical way of dealing with issues like these is a combination of two techniques:
52
55
@@ -57,17 +60,17 @@ The combination of these technique allows us to increases the likelihood that an
57
60
58
61
<!-- TODO: add an example here or even before -->
59
62
60
-
Such contract enforcement typically crash the application if their premise is not met, assuming that the only way this could have happened is if something before has already done horribly wrong.
63
+
Such contract enforcement typically crash the application if their premise is not met, assuming that the only way this could have happened is if something before has already gone horribly wrong, no recovery is possible, and the best way to move on is to die as quickly as possible.
61
64
62
-
This obviously needs careful considerations. You don't want all of the software in your car die at a random point in time without any recovery procedure.
65
+
This obviously needs careful considerations. You don't want all of the software in your car to die at a random point in time without any recovery procedure.
63
66
64
-
We won't talk about this too much but in general, as at least one potential reason for such failures is memory being in an undefined and potentially inconsistent state, people usually run multiple processes and monitor the main process by some watchdog that activates a safe recovery procedure if needed.
67
+
We won't talk about this too much but in general, as at least one potential reason for such failures is memory being in an undefined and potentially inconsistent state, people usually run multiple processes or even multiple programs on different hardware and monitor the main execution path by some watchdog that activates a safe recovery procedure if needed.
65
68
66
69
<!-- Add a fun video to this? Maybe laser or car? Or both? -->
67
70
68
71
## How to recover from recoverable errors
69
72
70
-
The bulk of this talk is focused around ways to recover from a recoverable error in modern C++. Here, a function is our smallest unit of concern.
73
+
The bulk of this talk is focused around ways to recover from a recoverable error in modern C++ with a function being our smallest unit of concern.
71
74
72
75
For the sake of example, let's say we have a function `GetAnswerFromLlm` that, getting a question, is supposed to answer all of our questions using some large language model living in the cloud.
73
76
@@ -77,22 +80,28 @@ For the sake of example, let's say we have a function `GetAnswerFromLlm` that, g
We've seen [functions](functions.md) like this before. This is a simple interface that serves its purpose in most situations: we ask it things and get some `std::string` answers (sometimes of questionable quality). But what if this function _cannot_ return an answer to our question? What should this function do in this case, so that we know that an error has occurred.
83
+
We've seen [functions](functions.md) like this before. This is a simple interface that serves its purpose in most situations: we ask it things and get some `std::string` answers (sometimes of questionable quality). But what if this function _cannot_ return an answer to our question? What should this function do in this case, so that we know that an error has occurred?
81
84
82
-
Largely speaking there are two schools of thought here:
85
+
Largely speaking there are three schools of thought here:
83
86
84
-
- It can throw an **exception** to indicate that some error has occurred
85
-
- It can return or set a special value to indicate a failure
87
+
1. It can throw an **exception** to indicate that some error has occurred
88
+
2. **It can return a special value to indicate a failure**
89
+
3. It can set a special global value to indicate a failure
86
90
87
-
### Why not throw an exception
91
+
Today we mostly focus on option 2., where we would return a special value of a special type to indicate that something went wrong, but before we go there, I'd like to briefly talk about why I don't like the other options.
88
92
89
-
We'll have to briefly talk about the first option here if only to explain why we're not going to talk about it in-depth. And I can already see people with pitchforks coming for me so do note that this is a highly-debated topic with even thoughts of [re-imagining exceptions altogether](https://www.youtube.com/watch?v=ARYP83yNAWk) as shown in this wonderful presentation by Herb Sutter.
93
+
### Why not set a global value
90
94
91
-
Anyway. Exceptions. Generally, at any point in our program we can `throw` an exception. This exception is then handled in a separate execution path, invisible to the user. Otherwise, `std::exception` is just a [class](classes_intro.md) like all those that we've seen before already. An exception object can be caught by value or by reference at any point in the program upstream from the place where the exception was originally thrown. Also, exceptions are polymorphic and use [runtime polymorphism](inheritance.md#using-virtual-for-interface-inheritance-and-proper-polymorphism), so there can be a hierarchy of exception classes and when exceptions are caught by reference, they can be caught by their base class.
95
+
We'll start with option 3 - setting some global value as an indicator for a failure. This way was quite popular long time ago but it rarely used today when we believe that variables should live in as local scope as possible. But you can still encounter it if you ever code using OpenGL, for example.
96
+
<!-- Check this and add an illustration -->
92
97
93
-
Essentially the problem comes down to exceptions using dynamic allocation at the throwing side and RTTI (Runtime Type Information) at the catching side. This means that technically a program can take an arbitrary amount of time to throw and catch an exceptions. In many domains where C++ is used, like in automotive, this is a non-starter.
98
+
### Why not throw an exception
94
99
95
-
In our case, if, say, the network would be down and our LLM of choice would be unreachable, the `GetAnswerFromLlm` could throw an exception, say a `std::runtime_error`:
100
+
A more interesting question is why not use option 1 - to throw an exception.
101
+
102
+
And I can already see people with pitchforks coming for me so do note that this is a highly-debated topic with even thoughts of [re-imagining exceptions altogether](https://www.youtube.com/watch?v=ARYP83yNAWk) as shown in this wonderful presentation by Herb Sutter.
103
+
104
+
Anyway. Exceptions. Generally, at any point in our program we can `throw` an exception. In our case, if, say, the network would be down and our LLM of choice would be unreachable, the `GetAnswerFromLlm` could throw an exception, say a `std::runtime_error`:
On the calling side, we would need to "catch" this exception using the `try`-`catch` blocks. Generally, if using exceptions for reporting errors, we wrap the code we want to execute into a `try` block that is followed by a `catch` block that handles all of our potential errors.
118
+
This exception is then "caught" in some other part of the program upstream of the place at which it was thrown using a so-called "try-catch" block. The exception travels to get there on a separate execution path, invisible to the user.
110
119
111
120
```cpp
112
121
intmain() {
@@ -121,26 +130,35 @@ int main() {
121
130
}
122
131
```
123
132
124
-
### Why I don't use exceptions much
133
+
<!-- Explain catch blocks -->
134
+
135
+
This sounds wonderful at first glance as it allows us to use the return type of our function for actually returning the result of the operation without trying to use it for anything else. This way also goes along the philosophy of having no unrecoverable errors: the function that throws an exception makes no decision about this error being recoverable or not - this will be decided by some other part of code that handles (or fails to handle) this exception.
125
136
126
-
I will not talk too much about exceptions, mostly because in around a decade of using C++ professionally I very rarely worked in code bases that use exceptions. Many code bases, especially those that contain safety-critical code, ban exceptions altogether due to the fact that there is, strictly speaking, no way to guarantee how long it takes to process an exception once one is thrown because of their dynamic implementation.
137
+
However, there are some limitations to this approach that we'll try to outline here.
138
+
139
+
#### Exceptions are expensive
140
+
141
+
A `std::exception` is just a [class](classes_intro.md) like all those that we've seen before already. An exception object can be caught by value or by reference at any point in the program upstream from the place where the exception was originally thrown. Also, exceptions are polymorphic and use [runtime polymorphism](inheritance.md#using-virtual-for-interface-inheritance-and-proper-polymorphism), so there can be a hierarchy of exception classes and when exceptions are caught by reference, they can be caught by their base class.
142
+
143
+
Essentially the problem comes down to exceptions using dynamic allocation at the throwing side and RTTI (Runtime Type Information) at the catching side. This means that technically a program can take an arbitrary amount of time to throw and catch an exceptions. Many code bases, especially those that contain safety-critical code, ban exceptions altogether due to the fact that there is, strictly speaking, no way to guarantee how long it takes to process an exception once one is thrown because of their dynamic implementation. In all the places where I worked the exceptions were either banned altogether or avoided when possible.
127
144
<!-- TODO: link Stack Overflow questionnaire about using exceptions -->
128
145
<!-- TODO: link to herb sutter's proposal: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0709r4.pdf -->
129
146
147
+
#### The hidden path is hidden
148
+
130
149
Furthermore, there is another thing I don't really like about them. They create a hidden logic path that can be hard to trace when reading the code.
131
150
You see, the `catch` block that catches an exception can be in _any_ calling function and it will catch a matching exception that is thrown at any depth of the call stack.
This typically means that we have to become very rigorous about what function throws which exceptions when and, in some cases, the only way to know this is by relying on a documentation of a function which, in many cases, does not fully exist or is not up to date. I firmly believe that the statement `catch (...)` is singlehandedly responsible for many errors of the style of "oops, something happened" that we've all encountered.
137
155
138
-
To be a bit more concrete, just imagine that the `LlmHandle::GetAnswer` function throws some other exception, say `std::logic_error` that we don't expect - this would lead us to showing such a `"Something happened"` message, which is not super useful to the user of our code.
156
+
To be a bit more concrete, just imagine that the `LlmHandle::GetAnswer` function throws some other exception, say `std::logic_error` that we don't expect - this would lead us to showing such a `"Something happened"` message, which is not super useful to the user of our code and still likely leads to the program to crash, which is what we tried to avoid with exceptions in the first place.
139
157
<!-- TODO: add an example of this -->
140
158
141
-
<!-- Old text below -->
159
+
### Use return type for explicit error path
142
160
143
-
### Avoid the hidden error path
161
+
<!-- TODO: old text below -->
144
162
145
163
All of these issues prompted people to think out of the box to avoid using exceptions. And that while still having a way to know that something went wrong during the execution of some code.
146
164
@@ -159,7 +177,7 @@ In the olden days (before C++17), there were only three options.
159
177
}
160
178
```
161
179
162
-
This option is not ideal because it is hard to define an appropriate "failure" value to return from most functions. For example, an empty string sounds like a good option for such a value, but then the LLM response to a query "Read this text, do not answer anything when done" would overlap with such a default value. Not great, right? We can extend the same logic of course for any string we would designate as the "failure value".
180
+
This option is not ideal because it is hard to define an appropriate "failure" value to return from most functions. For example, an empty string sounds like a good option for such a value, but then the LLM response to a query "Read this text, return empty string when done" would overlap with such a default value. Not great, right? We can extend the same logic of course for any string we would designate as the "failure value".
163
181
2. Another option is to return an error code from the function, which required passing any values that the function had to change as a non-const reference or pointer:
164
182
165
183
```cpp
@@ -175,7 +193,7 @@ In the olden days (before C++17), there were only three options.
175
193
}
176
194
```
177
195
178
-
This options is also not great. I would argue that not being able to have pure functions that get only const inputs and return a single output makes the code a lot less readable. Furthermore, modern compilers are very good at optimizing the returned value and sometimes the function that constructs this value altogether which might be a bit harder if we pass a reference to some value stored elsewhere. Although I don't know enough about the magic that the compilers do under the hood to be 100% about this second reason, so if you happen to know more - tell me!
196
+
This options is also not great. I would argue that not being able to have pure functions that get only const inputs and return a single output makes the code a lot less readable. Furthermore, modern compilers are very good at optimizing the returned value and sometimes the function that constructs this value altogether which might be a bit harder if we pass a reference to a value stored elsewhere. Although I don't know enough about the magic that the compilers do under the hood to be 100% about this second reason, so if you happen to know more - tell me!
179
197
<!-- In the comments below this video -->
180
198
3. An arguably even worse but still sometimes used method (OpenGL, anyone?) is to set some global error variable if an error has occurred and explore its value after every call to see if something bad has actually happened.
181
199
@@ -263,6 +281,8 @@ Now if we have a network outage, we can return an error that tells us about this
263
281
264
282
## Use `std::optional` to represent optional class fields
265
283
284
+
<!-- Maybe talk about this elsewhere. -->
285
+
266
286
As a a first tiny example, imagine that we want to implement a game character and we have some items that they can hold in either hand (we'll for now assume that the items are of the same pre-defined type for simplicity but could of course extend this example with a class template):
0 commit comments