What's the point of std:monostate? You can't do anything with it
devblogs.microsoft.com
devblogs.microsoft.com
I would've thought that the C++ template class would be just a marker interface to use on a monostate, so that users of the class know that it has shared state. But it seems like usage patterns in the article are very different from that, and all the comments here are ignorant of the history of the monostate pattern and befuddled at its intended usage. Maybe it was added to the standard by someone familiar with the design pattern, but they didn't do a good job with education and documentation to explain to everyone else what it was for?
[0] https://wiki.c2.com/?MonostatePattern
(originally discussed in http://ftp.math.utah.edu/pub/tex/bib/cppreport.html#White:19...)
> I would've thought that the C++ template class would be just a marker interface to use on a monostate, so that users of the class know that it has shared state.
Also, if that's what they really meant then they surely could've have written a far sorter comment that simply says this name is already taken and it should be called something else. They don't seem to be saying that at all.
(Not that it affects my original point, but FWIW that linked meaning of monostate isn't common in my experience, and it sounds like a truly awful idea: if your state is really good then be honest about it and use free functions. So it hardly seems worth reserving a useful word for it over the concept std::monostate is actually about.)
Numbers use = if you don't care about type. For instance, the real number 0 and the complex number 0 + 0i would be treated equal by this function.
Do you care about strict pointer equality? eq.
Do you want to distinguish 0 and 0d0? eql.
Do you care about isomorphism? equal.
The only weird function to me would be equalp, given that it performs case-insensitive comparison of characters etc. Overall, I find complaints about the equality predicates to be based on a misunderstanding of the definition of "equality".
Clojure goes the opposite direction and has a single = function that performs all sorts of reflection and deep comparisons; it's convenient in practice, but it's also easy to burn CPU cycles if you are e.g. comparing large collections and only care about pointer equality.
https://www.reddit.com/r/ProgrammerHumor/comments/225i15/pro...
(To contextualize and clarify: It's a classic old JavaScript joke about how Brendan Eich hates gay people getting married so much that he donated money to a political campaign against marriage equality, even though he enjoys the human right of marriage himself. Some bigots think they are more equal than others.)
It's more that LISP is the only language I know that exposes how complex the concept of equality actually is. All the other ones I use more-or-less make an implicit assumption that equality means one of the things that LISP offers but if you want the other definitions of equality you either have to build support for them by hand or call out to a library function that's going to use (static or dynamic) reflection to reduce the problem to the one kind of equality the language implements.
(Apart from that, the only other complaint is that LISP being as old as it is, the default names for those equalities are too terse to self-describe. You just have to memorize which one is 'eq' and which one is 'eql', there's no way to guess looking at the names themselves).
[1]> (equal '(1 2 3) '(1 2 3))
T
[2]> (equal "abc" "abc")
T
[3]> (equal #(1 2 3) #(1 2 3))
NIL
[4]> (subtypep 'string 'vector)
T ;
T
So a string is formally a kind of vector according to t he type system itself, but vectors don't use equal comparison whereas strings do. We can reach for equalp: [1]> (equalp #(1 2 3) #(1 2 3))
T
But then we have to accept, something we might not necessarily want: [2]> (equalp #(1 2 3) #(1 2 3.0))
T
which will not be true under the equal function that we really wanted to Just Work for simple vectors: [3]> (equal '(1 2 3) '(1 2 3.0))
NIL
eql being the default equality function throughout the library is also suboptimal.The specification allows (eq 1 1) to be false, which caters to unrealistically poor implementations such as ones that heap-allocate all integers. This, in spite of the fact that fixnum is required to be at least 16 bits wide: most-positive-fixnum must be at least 32767, and the fixnum type must be a supertype of (signed-byte 16).
What this means is that if Common Lisp were fit into a 16 bit machine (good luck with that at all), it would have to provide 16 bit fixnums (easily doable, but not unboxed ones) which defeats the fixnum concept of fixnums being the unboxed range of integers, beyond which we have boxed bignums.
vectors don't use equal comparison whereas strings do
This behavior is required by the specification. From https://www.lispworks.com/documentation/HyperSpec/Body/f_equ... , Two arrays are equal only if they are eq, with one exception: strings and bit vectors are compared element-by-element (using eql).
What makes strings and bit vectors special, in my opinion, is that they are homogeneous arrays. Heterogeneous arrays aren't something that I often use outside of Clojure (usually because they are awkward to use in statically typed languages). The specification allows (eq 1 1) to be false
Because the specification doesn't limit the magnitude of numbers nor state how they should be stored in memory. The scenario you've described requires an implementation that intentionally makes every number a bignum or an instance of a class (like Smalltalk).If you have list-based code that uses equal, you have extra work to do if you want to convert it to vectors.
Vectors comparable with equal make good hash table keys when heterogeneous keys are needed which don't need the structural flexibility of lists.
DonHopkins 85 days ago | parent | context | favorite | on: What if null was an Object in Java?
Why stop at null, when you can have both null and undefined? Throw in unknown, and you've got a hat trick, a holy trinity of nothingness! Of course the Rumsfeld Matrix further breaks down the three different types of unknowns.
https://en.wikipedia.org/wiki/There_are_unknown_unknowns
>"Reports that say that something hasn't happened are always interesting to me, because as we know, there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns—the ones we don't know we don't know. And if one looks throughout the history of our country and other free countries, it is the latter category that tends to be the difficult ones." -Donald Rumsfeld
1) Known knowns: These are the things we know that we know. They represent the clear, confirmed knowledge that can be easily communicated and utilized in decision-making.
2) Known unknowns: These are the things we know we do not know. This category acknowledges the presence of uncertainties or gaps in our knowledge that are recognized and can be specifically identified.
3) Unknown unknowns: These are the things we do not know we do not know. This category represents unforeseen challenges and surprises, indicating a deeper level of ignorance where we are unaware of our lack of knowledge.
And Microsoft COM hinges on the IUnknown interface.
https://en.wikipedia.org/wiki/Tony_Hoare#Research_and_career
>Speaking at a software conference in 2009, Tony Hoare apologized for inventing the null reference:
>"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." -Tony Hoare
https://news.ycombinator.com/item?id=19568378
>"My favorite is always the Billion-Dollar Mistake of having null in the language. And since JavaScript has both null and undefined, it's the Two-Billion-Dollar Mistake." -Anders Hejlsberg
>"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg
Assuming these mistakes are additive and not multiplicative.
`unknown` is a top type, since we're on the topic of nothingness it'd be remiss to not mention its dual, `never`.
The pair is also called `Any` and `Nothing` in some other languages.
- std::nullopt_t
- std::nullptr_t
- std::monostate
- std::tuple<>
And I'm sure there's more.
A function returning bottom cannot return, yet void foo() {} can. In fact it can even return the result of calling other void functions:
void bar() { }
void foo(){ return bar();}
In generic code void is usually internally replaced by a proper, regular void_t unit type and converted back to void at boundaries for backward compatibility. [[noreturn]] void bar();
would be a candidate for a bottom-returning function, except that [[noreturn]] isn't really part of the type system.I would almost rather argue that void is indeed the "bot" type, and a function marked with a void return type shouldn't be said to "return void;" rather we should say that it's an overloaded syntax that means the function has no return value at all. Same for "return bar()" there, that's just a false-friend of the syntax for returning a value, just syntactic sugar for "bar(); return;".
It's too late now. void pointers are used as a pun to mean "type wildcard." If void were a real thing that could have a size and address, that wouldn't work anymore.
Or you could say it the other way, that it is the bottom type, and the fact that it can be used as the unit type for returned values is a wart of the language. Furthermore, void* isn't a pointer of the unit type, it's a type for pointers to undefined/unspecified value types.
I think the original idea was that void meant "unknown", not "empty" or "non-existent": It was all about whether values could be allocated or not (the wart mentioned above). A plain void variable cannot, but a void pointer can be allocated. For functions, they just reused the keyword to mean "no return value" or "no arguments".
Hence, "void*" - a pointer to something, but it would be a syntax error to derefence this pointer.
Differently to other incomplete types void has some special behaviour: you can declare a function as returning void and return with no arguments is also special cased. You can also cast to void.
A void with proper unit semantics would simply be a complete type instead. The only special case would be return with no arguments implicitly returning a void instance, but that would be pure sugar.
C and C++ don't have a type spindle, where void would be at the bottom. Only C++ has the concept of subtype, only in the class system, and the C++ class system doesn't have a bottom type; there is no bottom class that is a base for all the others.
void is not a proper type; it's just a hack shoehorned into a convenient spot in the type system.
Which is why the C++ people have to invent this whole zoo of other things.
If void were a type, then, for starters, "return x;" would be syntactically valid in a function returning void. (Only, no possible x would satisfy the type system, so there would have to be a diagnosable rule violation in that regard.)
A function returning void does not return a type. It doesn't return anything; it is a procedure invoked for side effects.
The same situation could be achieved in other ways, like having a procedure keyword instead of void.
The (void) parameter list is another example of void just being a hack. It was introduced in ISO C, and then C++ adopted it for compatibility.
The 2023 draft of ISO C finally made () equivalent to (void), though it will probably take many decades for (void) to disappear.
A bottom type is not the base of all other types.
> void is not a proper type; it's just a hack shoehorned into a convenient spot in the type system.
It is a type, but it is not Regular and it is incomplete. 'return x;' is invalid in a void-returning function because it doesn't type check. 'return void()' or 'return (void)0;' or 'return void_returning_function();' are all valid because they type check.
Making void regular has been proposed multiple times [1]. It is a relatively simple extension but nobody that cares has the time to carry it through standardization.
[1] https://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0146r1...
6.8.7.5 The return statement
Constraints
1 A return statement with an expression shall not appear in a function whose return type is void.
It's a constraint violation. It doesn't matter what the type of the expression is.
It looks as if C++ made a small improvement here.
Yes, the bottom type is at the bottom of the type derivation hierarchy. That's why the word bottom is there; that's what it's at the bottom of. It's also why it can't have any instances. Since every other type is a supertype, then if the bottom type contained some value V, that value would be imposed into every other type! V would be a valid String, Widget, Integer, Stream, Array ... what have you.
But void is not the base of anything in C and C++.
You could argue that void is in some category of types where it is at the bottom; but no other types are in that category.
There is another problem: a bottom type should be the subtype of all types in that category. That includes being its own subtype. There we have a problem: C and C++ void is not a subtype of void in any sense.
As for the std::variant use case, using std::monostate is only a matter of convention there. You could use any of the other unit types just the same.
Using one type to represent empty literals for optional, tuple and pointer types, implicitly convertible to all of them, would make the compiler accept many obviously accidental constructs. In a world where the maintainers of C++ are trying their hardest to make the language safer what conceivable benefit would there be?
The word/type they wanted for this was the Unit type: https://en.wikipedia.org/wiki/Unit_type and in that list it even states the `std:monotype` is a Unit.
It's common enough that articles have been written against this usage: https://web.archive.org/web/20170224220253/http://jackieokay...
So this usage of functor may never have been "official", but it's widespread in the community.
† I don't know if the original mistake was his, but he certainly did a lot to spread it.
https://en.cppreference.com/w/cpp/experimental/reflect
https://en.cppreference.com/w/cpp/utility/variant/visit2
https://en.cppreference.com/w/cpp/experimental/parallelism
Visual Studio's official documentation uses the term functor as of 2021:
https://learn.microsoft.com/en-us/cpp/standard-library/funct...
A simple Google search shows no shortage of recent websites and documents including fairly authoritative references using the term functor to describe a type that overloads operator ().
Heck even the author of this article, Raymond Chen, uses the term functor as recently as 2020 to describe such a construct:
https://devblogs.microsoft.com/oldnewthing/20200513-00/?p=10...
Is Raymond Chen not a C++ expert?
The word "functor" has a long and glorious history in C++. Try entering "C++ Andrei Alexandrescu functors" into your internet search engine of choice. For bonus points, try "c++ Scott Meyers functor" as well.
The C++ usage was introduced by Jim Coplien in his 1992 book as a succinct name for an architectural pattern in C++: https://archive.org/details/advancedcbsprogr00copl/page/166/...
Like, if we assume someone looking at code with `std::unit`, what might they think this is? If one is not aware of its use in ML or similar, it could just as easily be assumed that it could be something to do with units like meters or kilograms or whatnot. After all, the C++ standard library is vast so it wouldn't necessarily be all that far-fetched.
Then the only question would be to ask why it would be default-constructible. At which point you'd have to read the docs for the type anyway.
And if my grandmother had wheels she'd be a bicycle. Of course if there wasn't a standard name for this concept that had been used in the industry for 50+ years (and in theoretical work for over 100) then it wouldn't make any difference what name you used for it. But given that there is a standard name for this concept that has been used in the industry for 50+ years, making up a different name is pretty unfortunate.
But to their dismay the crazier it gets, the more some people dig in and embrace it even more.
/sarcasm. I think.
Before C++ popularized the term, other programming languages and libraries had already used "vector" to describe similar data structures. For example, Common Lisp has a vector type that represents a one-dimensional array.
Interview with him acknowledging this:
And computing has interrupt vectors as well.
https://stackoverflow.com/questions/54336641/is-there-any-re...
Void doesn’t exist in ML
—- Haskell
data Void
(* Standard ML *)
datatype void = Void of void> a fancy way to say void?
Less fancy and more workable. Had void been a proper type in the first place it would not have been needed (but also… void had the same issue as nothing, it sounds like an uninhabited type more than a unit type).
Despite that, they could have called it Void, even if the standard library normally uses all lowercase.
In ML and friends monostate is called unit (and gets used a lot because void returns aren't allowed by the languages). Some have empty types too, which can never be occupied. A function returning Empty can't return, for example, though there are other use cases
But here we are talking about C++, where "void" is a pseudotype that is absolutely inhabited, in some conceptual sense. Any function that is declared to return void and which returns is returning a thing that conceptually inhabits void. In this sense, std::monostate indeed captures the same concept as void, but in a much better way, because it's properly a type, not a pseudotype.
Note: Java does the same thing, effectively, with "Void" which is inhabited by exactly one value: null.
I'd accept that it's not the same as the empty type though, given that void* can be occupied and functions marked void can return. Probably someone with more type theory than me can name this properly
void f();
void v = f();
void g(a){return a;}
v = g(v);
Maybe my imagination is failing me, but I can't see how this can do much good without at least polymorphic functions. template<range R, regular X, invocable<range_value<R>, X> F>
requires same_as<invoke_result_t<F, R, X>, X>
auto fold(R&& range, F f, X accumulator) {
for(auto x: range)
accumulator = f(x, accumulator);
return accumulator;
}
I can call that with a function returning a custom unit type: enum class void_t { Void };
fold(my_range, [](auto&& elem, void_t) { return Void; }, Void);
But not with void: fold(my_range, [](auto&& elem, void) { return; }, void{});
which is very annoying and requires fold to special case 'void' via metaprogramming. template<typename T>
T callWithState(auto f) {
auto old = globalState;
globalState = whatever();
T out = f();
globalState = old;
return out;
}
(forgive any syntax errors, my C++ is very rusty...) fn f() {}
let mut v: () = f();
fn g(a: ()) { a }
V = g(v);
> Maybe my imagination is failing me, but I can't see how this can do much good without at least polymorphic functions.Sure, it doesn’t do much useful to C save make void ever so slightly less wonky.
If following something like CQS the bifurcation can be thought of allowing “pure” functions and excluding code with a temporal / side-effecting component from higher order code.
Not saying bifurcating on void is the best approach to handle that, but in languages where side effects are a thing something is needed to make sure higher order code and side effecting code mix properly.
And this doesn’t come anywhere near to properly making that distinction anyway: a non-void function can have all the side effects, and a void function can have no side-effects.
It also does not “make sure higher order code and side effecting code mix properly”, it just makes a subset of likely side-effecting code not mix with higher order code at all.
In C++, there's a very compelling case for making void an actual type, because you can't use void as a templated type, which means that templates involving functions that potentially have void return types require unpleasant amounts of template metaprogramming.
Now that C++ standards committees are considering basic usability fixes (e.g. the long overdue ability to do`namespace com::microsoft::directx { }`) there's a vague possibility that somebody might look into actually fixing this some time before 2040.
C++17 and C++20 both took up usability fixes. std::string::ends_with would be a a C++20 example.
Hopefully C++23 will be similarly open to usability fixes.
Probably “garbage”. C’s void is not a type and does not behave consistently, it’s a keyword associated with arbitrary convenient behaviours for that case.
The compiler does not allow you to do that particular operation out of an arbitrary restriction, but that does not make `void` a true void type. It still holds a monotype value!
int bar() {
std::cout << "Bar" << std::endl;
return 0;
}
void baz() {
std::cout << "Baz" << std::endl;
}
template <typename T> T foo(T (*f)()) {
std::cout << "Foo" << std::endl;
return f();
}
template <typename T> T varfoo(T (*f)()) {
std::cout << "Varfoo" << std::endl;
T a = f();
return a;
}
int main()
{
foo(bar); // Valid (returning an int).
foo(baz); // Valid (returning a void).
varfoo(bar); // Valid (assigning an int).
// varfoo(baz); // Invalid (assigning a void (why???)).
}In category theory terms, I believe void is the initial type (there is exactly one morphism from void to any other type), whereas monostate is the terminal type (there is exactly one morphism from any other type to monotype).
template<typename T = std::monostate>
class C
{
...
if constexpr (!std::is_same_v<T, std::monostate>
{
// T-related behaviour here
}
};Anyway, I write basically this on my blog for a more thorough explanation: https://www.jackyoustra.com/blog/non-equatable-void
Hans: No -- it does nothing.
This thing is just a counterpart to void that is a class. A better name would be voidclass or something along those lines.
(There is a Monostate pattern, but that involves a class with state: just all the state is static. It's basically like a module with global variables. Completely different thing.)
But in a way, this is right since:
<no of bits>
2 = <no of states>
When the number of bits is 0, 2^0 = 1: 1 state. A state machine with one state is certainly possible.Problem is we need 2 or more states to do anything useful with state. We can draw an initial state bubble in a state diagram and not add any states; it can even have transitions back to itself.
So maybe monostate is not exactly a misnomer; it's just weird to mention state about something that is not useful for working with state.
Not everything can or should be default-constructed or default-constructible. That does complicate initialization sometimes (i.e. there are practical reasons to "suspend" construction until you have all the needed data), but you're not avoiding breaking type safety by adding a `monostate`, you're just giving yourself the "could be null" headache.
In my mind, a computation (a “function”) must either return a value or hang/crash. If it appears as though it returns a member of ∅, it must hang/crash, because there are no members of ∅. If it returns a member of the single-element set, [1], it can return one, there’s just no use inspecting it afterwards (you know what it is already).
(For what it’s worth, if you use a prover-adjacent language such as Agda or Idris, this is exactly how things are going to work there.)
It can also simply return control, without returning anything. It's equivalent to invoking a continuation with zero arguments. Do you allow for zero-argument functions, at least?
Of course, if a function can't return no value whatsoever, you suddenly need new syntactical categories to support it: you need to prohibit using such functions in an expressions (only call statements are allowed), you need a way to return from such a function (naked "return", which is prohibited from taking any expressions), and it's also severely strains your generics/templates because you can't treat such functions uniformly etc.
A function on a zero type would be unable to return any value, since there's no value you could apply it to. To have a function of zero arguments you should use the unit type (which means the function effectively picks out a single value).
This is also related to how a function with multiple arguments is a function of the product type, and an empty product is 1 not 0.
Sure, you can build your whole theoretical framework of computation with only the functions of exactly one argument, and then deal with tuples to fake multi-valued arguments/multiple return values — but you don't have to do that. You may as well start from the functions with arbitrary (natural) number of arguments/return values, it's not that hard.
I mean surely we can agree that a pure function of 0 arguments picks out exactly 1 value, and that a function that accepts n different values (values not arguments) as input returns at most n different results? Why make an exception for n=0?
Your definition of a function of 0 arguments and that of a function over the unit set are identical. Or at least equivalent.
It's yet another example of "in theory, the theory and the practice are the same; in practice, they're different": I have written a toy functional language that compiles down to C, and unit-removal (e.g. transforming int*()*int into int*int, lowering "fun f() -> whatever = ..." into a "whatever f(void) {...}" etc.) is a genuine optimization. The same, I imagine, would apply to generating raw assembly: you want to special-case handling of unit so that passing it as an argument would not touch %rdi, and assigning a unit to a value should not write any registers, and "case unit_producing_function(...) of -> ... end" actually has no data dependency on the unit_producing_function etc.
Alternatively, you may instead represent () as a full, 64-bit wide machine word and then map every 64-bit value to mean () so, again, you don't actually need to write anything: all registers contain a valid representation of () at all times. This is similar to how we usually represent booleans: 0 is mapped to mean False, and everything else is mapped to mean True, although in this case we sometimes do need to rematerialize some definite value into the register of choice.
In any case, it's mostly just a matter of correctly writing the constant materializer; but if you adopt multi-argument/multi-valued functions you simply never encounter this problem:
for arg, place in zip(arg_list, arg_places):
load(arg, place)
invoke(fun, kont)
for val, place in zip(ret_values, ret_places):
load(val, place)
kontinue(kont)
Notice how degenerate loops simply disappear with no additional handling.Sure. If you have multiple return then unit values become a lot less important because you can just have a function that returns 0 values. But most languages, especially C-like languages, do not have multiple return.
You don't have to push anything for the unit values on the stack though, their representation doesn't take up any space. Just like if you're passing a big argument (like a large integer, or a struct passed by value) it might take multiple registers or a lot of space in your stack frame, there's no 1:1 correspondence between arguments and stack space.
> you want to special-case handling of unit so that passing it as an argument would not touch %rdi, and assigning a unit to a value should not write any registers
This shouldn't need to be a special case though. For each datatype you have a way of mapping it to/from some number of registers, and for the unit type that number of registers is 0 and that mapping is a no-op.
One of the way to represent them doesn't take up any space. But if you want to e.g. have a generic function of e.g. (T,U,V) -> V type (that is, with no monomorphization in your compiler) then either your units have to take space (otherwise the layout of (unit,int,bool) is different from (int,int,bool) and the same), or you'll have to pass around type descriptors when invoking generic functions. Which many, many implementors of static languages rather wouldn't do unless absolutely necessary.
You have that problem already though surely, as T might be bigger than an int, or want to be passed in a floating-point register, or both.
There's nothing weird about a function taking 0 arguments, any more than there's anything weird about a function taking 3 or 5 arguments. A function can't take an argument of type void, so functions shouldn't be allowed to return void either.
> Of course, if a function can't return no value whatsoever, you suddenly need new syntactical categories to support it: you need to prohibit using such functions in an expressions (only call statements are allowed), you need a way to return from such a function (naked "return", which is prohibited from taking any expressions), and it's also severely strains your generics/templates because you can't treat such functions uniformly etc.
Or you do what sensible languages do: all functions return a value (if they return at all), functions that don't want to return anything in particular can return a unit value but that value is just a normal value that behaves normally in expressions etc., you don't make naked return a special case (you can make it syntax sugar for "return unit" if you really want), and you can treat those functions uniformly in generics, much more easily than with C++ templates where you have to make special cases for void functions.
We also have "void foo(void)" and here void takes on two entirely different meanings, while type theory would suggest this is a function that diverges if it were called, which you can't.
- On one hand, a lot of "functions" are actually procedures that just happen to return a value: think for example `write(2)`, which is clearly used for its side effect, not to compute how many bytes could be written -- even though that's what it returns.
- On the other hand, you can have a "procedure" (i.e., a function "returning" void) that actually has no side effects other than storing a computed value in a specified location (e.g. void square(int x, int *ret) { *ret = x*x; }). That's clearly a function in the mathematical sense, even though it "returns void".
In fact the traditional programming name for what we mostly call functions today was "(sub)routine" - you call a subroutine, and when it finishes, it returns to where it originally started.
Consider also that at the assembly level (and below it), subroutines don't have return values, nor arguments. The program counter simply jumps to the beginning address of the subroutine, and the `return` instruction jumps back to the address right after that jump. The subroutine may read values from certain locations in memory, and possibly write some others back, but none of this is necessary or enforced in any way. C functions, and the corresponding keywords, are much closer to this conception of assembly subroutines than they are to the mathematical notions of functions or computations.
You don't even have to go that far, Rust supports this concept. The built-in empty type is called `!`, and cannot be constructed. It's partially unstable, and there's a bunch of things you can't do yet, but you can use it as a return type.
If you want to go with math, think more group theory. I'm specifically thinking about how you can always create a monoid if you have an associative binary operation, because even if your associative binary operation doesn't have an identity element, you can just declare one. Similarly, if you have "functions" that "return nothing", you just declare that nothing right into existence. Then you can just think of the C language layer basically erasing away any attempt to examine that value returned behind the scenes, because as you say, why?
No need to go that far, you just need an ML inspired language with subtyping.
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-nothing... https://www.scala-lang.org/api/2.13.6/scala/Nothing.html https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/ https://www.scala-lang.org/api/2.13.6/scala/Unit.html
std::variant<int, whatever> v;
// v is zero
and std::variant<int, whatever> v = 0;
And don't want to pay the space and time overhead of wrapping in an std::optional which will use a whole other byte at least for not good reasonThis. monostate's behavior should be captured in the std::optional spec. There's no need to create a new type.
Edit: My point, if not clear, is that the compiler should add the extra bit of code for when an optional is empty automatically, rather than requiring that an optional be defined (it's optional!) unless it is explicitly typed as monostate.
Brevity, I guess? I suppose the most brief way to express absence or presence of value in C++ is a pointer, but I could be tried at the Hague for that take.
None of this is to be facetious, I'm really trying to learn here. I'm not a C++ guy by trade. C and Rust are my day-to-day languages.
std::variant<std::monostate, T, U, V>
Or std::optional<std::variant<T, U, V>>
But then the "none" state is a bit more "special", it's not just one of the options. The sizeof of the type will also probably be a bit bigger, because it has to contain both the bool for the optional, as well as the tag for the variant.The other obvious use is what the article states, that it allows the variant to be default constructed even if none of its members are. Though you can do that with optional as well. It's mostly a matter of style. I mostly avoid std::monostate because the name is so confusing, I really agree with the other users that something like std::unit or std::none would be better.
Ah, okay! So it's serving the same purpose as the "None" in this Rust snippet:
enum Foo {
None, // <-- impl core::Default and return this
T(T),
U(U),
V(V),
}
That makes sense, though it really shows how the legacy of C++'s type system limits stdlib design in the present day. It'd be awfully nice to be able to just write std::variant<void, T, U, V>.Fundamentally, Rust’s enums are a much better way of doing this thing compared to std::variant, but C++ did the best it could without changing the core language. Which arguably they should have done.
The size of an optional<T> is `sizeof T + 1` because it needs a boolean for the set/unset flag.
The size of a variant is… the same, at least, because it needs to store a discriminator integer (apparently both gcc and clang do optimise to a byte when there are less than 256 variants).
Or do you mean adding a monostate to an existing variant rather than wrap that variant into an `optional`? In which case you should fix your example (and provide a version wrapped in an optional) because they’re very confusing.
Current situation: you have
std::variant<int, whatever> x;
you now want to discriminate on whenever x has been initialized explicitly or not, the two cases I posted: // case 1
std::variant<int, whatever> x;
// case 2
std::variant<int, whatever> x = 0;
you have two options:option A: does not needlessly increase sizeof, does not add indirection penalty upon access:
std::variant<std::monostate, int, whatever> x;
option B: needlessly increases sizeof, adds indirection penalty upon access (mostly relevant when compiling in debug mode without inlining if you still want to have a semblance of performance): std::optional<std::variant<int, whatever>> x;