The problem is… in C++, this style is just a convention, _ is still a regular variable.
So if we want to ignore two value with different type, it does not work since the type mismatch.
1 2 3 4 5
voidfoo(){ auto [_1, done1] = someJob<int>(); auto [_2, done2] = someJob<std::string>(); // we need to separate _2 from _1 }
That’s frustrating, especially for people with experience of pattern-matching expression in
other languages.
So in C++26 (proposed by P2169), now we can new way to interpret the semantic of _.
The rule is simple.
If there is only one declaration of _ in some scope, everything is same as before.
A we can reference it later if we wan’t, although it’s probably a bad smell to use _ in this case.
If there are more declarations of _, they all refer to different objects respectively.
In this case, they can only be assigned to. Try to use them is a compiling error.
And we can finally write something that looks more natural.
1 2 3 4
voidfoo(){ auto [_, done1] = someJob<int>(); auto [_, done2] = someJob<std::string>(); }
Golang has this feature from the beginning, is called blank identifier.
For Python, although being a dynamic-type language, there is no problem to do use _ for different
type value. _ is defined as a wildcard when pattern-matching is introduced to Python (PEP 634).
But it’s intended to be a storage accessed through network, so any operation on it is
inherently going to fail at some time.
Also, We are 2024 now, loving FP, preferring expression over statement, monadic operation
being so cool.
Thus we decide to wrap all return type in std::optional to indicate these actions may fail.
...\include\optional(100,26): error C2182: '_Value': this use of 'void' is not valid
Well. template stuff.
What Happened?
The problem is that void is an incomplete type in C/C++, and always to be treat specially
when we are trying to use them.
By incomplete in C/C++, we mean a type that the size of which is not (yet) known.
For example, if we forward declare a struct type, and later define it’s member. The struct type
is incomplete before the definition.
1 2 3 4 5 6 7 8 9
structItem;
Item item; // <- invalid usage, since that the size of Item is unknown yet.
structItem { int price; };
Item item; // <- valid usage here.
And void is a type that is impossible to be complete by specification.
But we can have a function that return void?
Well, we return nothing
1 2 3
voidfoo(){ }
voidfoo(){ return; } // Or explicit return, both equivalent.
BTW, C before C23 prefer putting a void in parameter list to indicate that a function takes
nothing, e.g. int bar(void), but is’s kinda broken design here.
Since that we can not evaluate bar(foo()). There is no such thing that is a void and exists.
Conceptually, std::optional<T> is just a some T with additional information of value-existence.
1 2 3 4 5
template <typename T> structMyOptional { T value; bool hasValue; };
Because that there is impossible to have a member void value,
std::optional<void> is not going to be a valid type at first place.
(Well, we can make a specialization for void, but that’s another story.)
So, How can We Fix?
The problem here is that there is no a valid value for void in C/C++.
At some level, program can be though of a bunch of expressions. An running a program
is just the evaluation of these expressions. (Also the side effects, for real products / services)
The atom of expression is value. If there is a concept that’s not possible to be express
as a value, we are kicking ourselves.
Take Python for example, if we have a function that return nothing, then the function actually
returnsNone when it exits.
1 2 3 4
deffoo(): pass
assert(foo() isNone) # check pass here
1 2 3 4 5 6 7
deffoo(arg: None) -> None: pass
defbar(arg: None) -> None: pass
bar(foo(None)) # well.. if we really want to chain them together
So nothing itself is a thing, in Python, we call it None.
Every expression now is evaluated to some value that exists, the the type system build up that
is complete at any case.
The concept of nothing itself here is call unit type in type theory.
It’s a type that has one and only one value.
In Python, the value is None, in JavaScript it’s null, in Golang … maybe struct{}{} is a
good choice, although not standardized by the language.
Unit Type in C++
Now is the time for C++. As we already see, void is not a good choice for unit type because we
can not have a value for it. Are there other choices here?
Just define a empty struct and use it probably not a good choice, since that
now our custom unit type is not compatible with unit type from other code in the product code base.
How about nullptr, that’s the only one value for std::nullptr_t.
(So the type is std::optional<std::nullptr_t>).
It’s a feasible choice, but looks weird since that pointer implies indirect access semantic,
but it’s not the case when using with std::optional<T> here.
How about using std::nullopt_t? It’s also a unit type but it’s now more confusing.
What’s does it mean by std::optional<std::nullopt_t>? A optional with empty option?
There is a static assert in std::optional<T> template that forbid this usage directly,
probably because it’s too confusing.
Maybe std::tuple<>? A tuple with zero element, so it have only one value, the empty tuple.
That seems to be a good choice because the canonical unit type in Haskell is () the empty tuple.
So it looks natural for people came from Haskell.
But personally I don’t like this either since that now the type has nested angle bracket
as std::optional<std::tuple<>>.
There is a type called std::monostate, arrived at the same time as std::optional in C++17.
This candidate do not have additional implication by it’s type or it’s name.
It’s monostate! Just a little wordy.
std::monostate is originally designed to solve the problem for a std::variant<...> to be
default initialized with any value. But it’s name and it’s characteristic are all fit our
requirement here. Thus a good choice for wrapping a function that may fail but
return nothing.