[HN Gopher] What's the point of std:monostate? You can't do anyt...
___________________________________________________________________
What's the point of std:monostate? You can't do anything with it
Author : luu
Score : 84 points
Date : 2024-07-19 02:15 UTC (3 days ago)
(HTM) web link (devblogs.microsoft.com)
(TXT) w3m dump (devblogs.microsoft.com)
| pavlov wrote:
| There's a secret competition between C++ and JavaScript to come
| up with more variants of nothing.
| ThrowawayTestr wrote:
| Mathematicians got nothing on lazy programmers
| immibis wrote:
| VBA has 4 kinds. Null, Nothing, Missing, and Empty.
| shadowgovt wrote:
| If you like that, you'll love how many ways you can test for
| equality in LISP.
| MathMonkeyMan wrote:
| That always chafed me about Scheme. I see the utility of
| `eq?` and `eqv?`, but I'd prefer that there were only
| `equal?` and functions were defined to get an object's "id"
| or "numeric equivalency class," or whatever, instead of
| having different flavors of data structures that differ only
| for certain values.
| loa_in_ wrote:
| It's probably more of use to compiler/runtime developers than end
| users if the language.
| jcelerier wrote:
| It's useful whenever you want a variant with a default state
| which is different from any value you'd actually put it it, to
| differentiate between e.g. 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 reason
| glitchc wrote:
| > 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 reason
|
| This. monostate's behavior should be captured in the
| std::optional spec. There's no need to create a new type.
| masklinn wrote:
| That is opposite of sense. monostate is a much more general
| concept, if you want to argue about the need for a new
| type, the answer is to remove optional because it's a
| `variant<T, monostate>`. It doesn't work the other way
| around.
| glitchc wrote:
| That's putting the cart before the horse. Languages are
| designed for developers to use, not compilers to
| optimize. The intention and use of std::optional is
| clear.
|
| 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.
| masklinn wrote:
| That makes even less sense, what "extra bit of code" are
| you talking about? monostate is not designed to be used
| with optional: although there's the odd case where that's
| useful an optional<monostate> is bijective to a boolean,
| and because C++ does not have zero-sized types it's less
| efficient (as it takes two bytes).
| ryukoposting wrote:
| Parton my naivete, but if you wanted a value that's either a
| thing, or not-a-thing, why wouldn't you express that with a
| std::optional? What advantage does std::monostate have versus
| option types?
|
| 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.
| OskarS wrote:
| You're not wrong, if you just want "a T or nothing",
| optional is the way to go. But what if you want "a T, a U,
| a V, or nothing"? Then you do
| 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.
| ryukoposting wrote:
| > it allows the variant to be default constructed even if
| none of its members are
|
| 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>.
| jcelerier wrote:
| I mean, it's very trivial to make your own basic wrapper
| with "void" in the API if that's what you want:
| https://gcc.godbolt.org/z/v6EPTaaPY
| ryukoposting wrote:
| I suppose that code is "trivial" in the same sense that
| anything else involving C++11 is "trivial."
| masklinn wrote:
| That... doesn't make any sense?
|
| 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.
| jcelerier wrote:
| I don't understand what's confusing with my example aha.
|
| 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;
| jandrewrogers wrote:
| It has other uses for end-user developers. For example, when
| you need a class member to be conditionally elided based on
| template parameters. You can swap the normal type with non-zero
| size with std::monostate such that it has zero size.
| tpolzer wrote:
| A std::monostate member will still have non zero size,
| because it needs a unique address.
|
| See https://en.cppreference.com/w/cpp/language/ebo
| jandrewrogers wrote:
| ...except if you use the standard [[no_unique_address]]
| attribute.
| ramon156 wrote:
| So the upside is that you can put it at the beginning of a
| function? Wild, bro
| Joker_vD wrote:
| You can also use it in any template without any trickery unlike
| void, which I am willing to bet was the actual rationale.
| meibo wrote:
| The article describes that as the main advantage, showing
| std::variant as an example.
| Smaug123 wrote:
| Having access to the unit type _is_ useful; I use it maybe once
| every couple of months in F# even if we restrict solely to the
| use-case "instantiate a generic with the unit type to indicate
| that no data is held here". (Of course, since F# also doesn't
| have a `void` type - a truly non-constructible data type is
| indeed _very_ rarely useful! - F# uses `unit` in many places
| where C++ and C# use `void`.)
| lloydatkinson wrote:
| So it appears this follows the terrible C++ "naming things with
| the wrong name on purpose" trend, with the biggest example being
| naming a variable length array (a list, collection, etc)
| `std:vector` even though Vector was already a well known word
| with a very different meaning.
|
| 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.
| codeflo wrote:
| Functor is one of the worst offenders IMO. In C++ lingo, it
| simply means "callable object".
| halayli wrote:
| In C++ lingo they are called function objects or callables,
| not functor. You might stumble on code here and there that
| suffix a class name with Functor but it's not that common.
| codeflo wrote:
| Many blog posts and Stackoverflow questions about C++ cal
| them functors. Boost of all things uses the term in its
| definitions: https://www.boost.org/doc/libs/1_60_0/doc/html
| /function/refe...
|
| 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.
| plorkyeran wrote:
| I would say it _was_ widespread, but it 's now fallen out
| of favor. Alexandrescu got the name wrong 25 years ago+
| and that got embedded into early boost, but by the time
| that blog post was written I feel like I'd mostly stopped
| seeing it being used by C++ experts.
|
| + I don't know if the original mistake was his, but he
| certainly did a lot to spread it.
| Maxatar wrote:
| cppreference.com uses the term functor in numerous places
| for recent C++ features including things in C++26 such as
| reflection:
|
| https://en.cppreference.com/w/cpp/experimental/reflect
|
| https://en.cppreference.com/w/cpp/utility/variant/visit2
|
| https://en.cppreference.com/w/cpp/experimental/parallelis
| m
|
| 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?
| sgbeal wrote:
| > In C++ lingo they are called function objects or
| callables, not functor.
|
| 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.
| Maxatar wrote:
| The creator of C++ uses the term functor in the book "The
| C++ Programming Language" to describe any object that
| overloads operator ().
| layer8 wrote:
| It's not like anyone has a monopoly on the term:
| https://en.wikipedia.org/wiki/Functor_(disambiguation)
|
| 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/...
| sham1 wrote:
| I'd argue that unit would be just as cryptic as monostate, if
| you don't know what either is.
|
| 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.
| mananaysiempre wrote:
| It's called "unit" because it is a unit of the operation of
| multiplication / constructing a tuple (up to a unique
| isomorphism): for any type 't the tuple types unit * 't and
| 't * unit are isomorphic to 't. When you don't have that as a
| fundamental operation in your type system, like ML does, then
| it could be confusing.
| xdavidliu wrote:
| sure, but I would be surprised if a significant fraction of
| programmers knew that. When we hear the word "unit", that's
| not the first definition that comes to mind.
| omnicognate wrote:
| There is indeed a units library [0] aiming for
| standardisation in C++29.
|
| [0] https://github.com/mpusz/mp-units
| Scubabear68 wrote:
| My guess is the maintainers of C++ keep making it more and more
| awful in an attempt to force people to more sane languages.
|
| But to their dismay the crazier it gets, the more some people
| dig in and embrace it even more.
|
| /sarcasm. I think.
| andersa wrote:
| It's not sarcasm. The maintainers have completely lost the
| plot.
| IncreasePosts wrote:
| Even Herb Sutter agrees(I think, in his heart, even if he
| doesn't come out directly and say it), which is why he is
| working on cppfront
| atoav wrote:
| Coming from other languages I noticed this about C++ as well. I
| can't give examples right now but I recall multiple times being
| like: "Oh that is just a weird name for
| $KnownComputingConcept".
| halayli wrote:
| The term "vector" represents an ordered collection of
| elements(ex 3-dim vector is [x,y,z]). A list is flat out
| incorrect because it is used to refer to linked
| list(std::list), and a collection means a group of objects and
| that can be anything like a map, set, etc so it's too generic.
|
| 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.
| Maxatar wrote:
| Stepanov introduced the term in C++ and he fully acknowledges
| that it was a bad name and that he regrets it. If he could
| redo it, he would have renamed it array or array_list.
|
| Interview with him acknowledging this:
|
| https://www.youtube.com/watch?v=etZgaSjzqlU
| Y_Y wrote:
| It doesn't help that mathematics and physics have their own
| overlapping but unequal definitions of "vector".
| yndoendo wrote:
| Reactive style programming also takes the Unit as the value to
| act upon when the value and the type does not actually matter
| and only the action does.
|
| https://stackoverflow.com/questions/54336641/is-there-any-re...
| gpderetta wrote:
| Consider ML appropriating the term tensor...
| nmeofthestate wrote:
| Another use of std::monostate is as a special "unset/don't care"
| value for a template parameter. eg
| template<typename T = std::monostate> class C {
| ... if constexpr (!std::is_same_v<T,
| std::monostate> { // T-related
| behaviour here } };
| ot wrote:
| You can use void for that.
| Maxatar wrote:
| void could be an actual meaningful type for a template rather
| than a dummy type.
| klyrs wrote:
| I greatly prefer tag classes for this. You can define 'em in a
| single line and a downstream user can't accidentally plug the
| unset value in to the template.
| amne wrote:
| so "std::nothing" was taken? or "std::none" ? or anything else
| that would make it obvious this type is a fancy way to say void?
| rwmj wrote:
| Or "unit" in ML-derived languages and Haskell.
| fire_lake wrote:
| I always think of unit as one - it has exactly one possible
| value.
|
| Void doesn't exist in ML
| ackfoobar wrote:
| log 1 = 0, so it has exactly zero bits of information.
| millimeterman wrote:
| Void definitely exists in ML. --- Haskell
| data Void (* Standard ML *) datatype void
| = Void of void
| masklinn wrote:
| "Nothing" can easily be interpreted as an uninhabited type
| (regardless of its use in haskell).
|
| > 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.
| ackfoobar wrote:
| Capital letter `Void` reminds me of how Java uses the object
| type `Void` for this purpose, as all reference types allow
| null.
| proaralyst wrote:
| Void is different from this type though, as a variable of type
| void can't be occupied.
|
| 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
| cvoss wrote:
| You are equivocating on the word "void". Your statement that
| "a variable of type void can't be occupied" is true in
| functional languages where "void/Void" is often used as the
| name of a type that isn't inhabited (assuming the language is
| sound/normalizing/whatever).
|
| 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.
| proaralyst wrote:
| I think it's not correct to say that void is a monotype in
| C++, because the compiler won't allow you to assign the
| result of a function marked void to a variable, and you
| cannot declare a variable of type void.
|
| 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
| saurik wrote:
| Which is really annoying, and makes a ton of templated
| code in C++ have have to bifurcate on void unnecessarily.
| They already let me do return f(); in a void function if
| f also returns void... they should let me declare a
| variable of type void and the language is going to become
| a lot more pleasant.
| Y_Y wrote:
| What are you going to put in that variable?
| 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.
| gpderetta wrote:
| 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.
| remexre wrote:
| Templates already provide that useful polymorphism;
| 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...)
| CuriousSkeptic wrote:
| Not that familiar with c++ but used to have this thought
| about both Java and C#. Think I've changed my stance on
| it now though.
|
| 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.
| ackfoobar wrote:
| I'm not that familiar with C, or C++. My impression is
| that void is a special case that doesn't need to be
| special, some accidental complexity that came from
| mapping machine instructions to a higher level language.
| rerdavies wrote:
| [delayed]
| Koshkin wrote:
| Incidentally, the classic C did not have 'void'; instead,
| it was assumed that any function would, by default, return
| 'int' in the form of some value stored in the accumulator,
| and so the "value" of 'void' would be effectively
| represented by random garbage. The 'void' that was
| introduced explicitly in a later version of C weakened the
| original meaning of the unknown value by allowing pointers
| to 'void' and thus not requiring that the value pointed to
| must be always thought of as meaningless (since you could
| cast a pointer to void to a pointer to something else).
| kevinventullo wrote:
| I think there is exactly one equivalence class of instances of
| std::monostate, whereas there are exactly zero equivalence
| classes of instances of void.
|
| 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).
| rerdavies wrote:
| std::void_t would have been nice.
| revskill wrote:
| Is it like a singleton without any method ?
| masklinn wrote:
| Since it doesn't have any data either (hence mono _state_ )
| that's not a useful distinction. It's a singleton in the same
| way 1 is a singleton.
| formerly_proven wrote:
| Seems almost intentionally confusingly written. Why not use void
| if monostate is like void? Ah, because monostate is actually
| entirely unlike void. void has zero values, mono state exactly
| one.
| leni536 wrote:
| You can have as many monostate objects as you want.
| mananaysiempre wrote:
| void is kind of strange in C, because it looks like an empty
| type, but it still behaves as though it has a single instance
| (a unit type). For example, a function with an empty return
| type can't return (it'd have to supply a value of it); a void
| function can. You can't cast things to an empty type (otherwise
| you'd get a value of it), you can cast things to void. Void a
| unit type, not an empty type, it's just a bad one.
| formerly_proven wrote:
| It would be somewhat more cohesive and less weird if we'd
| argued that the "void" return type means that a return value
| cannot be constructed which is taken to mean that the
| function has no return value, and simply returns without
| providing a value.
| mananaysiempre wrote:
| Try as I may, I can't make sense of that. I've read
| something like it in books on C, but I still can't. Maybe
| I'm infected with set theory too deeply.
|
| 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.)
| Joker_vD wrote:
| > a computation (a "function") must either return a value
| or hang/crash.
|
| 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.
| shiandow wrote:
| You encounter the same problem with zero argument
| functions.
|
| 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.
| Joker_vD wrote:
| No, you just invoke the function and pass it zero
| arguments, that's it.
|
| 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.
| shiandow wrote:
| Sure, and passing it zero arguments is exactly what it
| means to evaluate it on the single value of the unit set.
|
| 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.
| Joker_vD wrote:
| They're equivalent, but only up to whatever computational
| substrate one is actually using. You can build functions
| out of small-step operational semantics of, say, a
| simplistic imperative register machine with a stack. In
| this case, a function of 0 arguments and a function of 1
| trivial unit argument are visibly different even though
| their total effect on the state is the same. After all,
| we're talking about theory of _computation_ and so it
| better be able to handle computations as they are
| actually performed at the low level, too.
|
| 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.
| shiandow wrote:
| How would one write a 0 bit value into registers?
| Joker_vD wrote:
| Yes, that's the problem you face when you're dealing
| strictly with functions of a single argument. Still,
| there are two options: first, it's arguably is already
| written into the register so you don't need to do
| anything.
|
| 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.
| formerly_proven wrote:
| It doesn't make sense from a strict type and set theory
| point of view because it doesn't make sense from a strict
| type and set theory point of view. Neither C nor C++ are
| rigorous languages.
|
| 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.
| masfuerte wrote:
| It's not a function. A C function "returning" void is
| just C's syntax for writing a procedure. C doesn't call
| it a procedure, but that's what it is semantically.
| moefh wrote:
| The distinction between "function" and "procedure"
| doesn't map very well to whether the return type is void
| in C's syntax:
|
| - 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".
| simiones wrote:
| C functions (and in fact the "functions" of most
| mainstream programming languages) are not computations,
| they are algorithms. An algorithm doesn't necessarily
| have a result of any kind, at least not in the way that a
| (mathematical) function has. The result of the algorithm
| can be the state in which it leaves the World (e.g. an
| algorithm for cleaning a house doesn't have a return
| value, it changes the state of the house).
|
| 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.
| MobiusHorizons wrote:
| The calling convention does say where to look for the
| return value. So in a sense the return value always
| exists, but would not be meaningful if the function has a
| void return type.
| Measter wrote:
| > (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.)
|
| 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.
| jerf wrote:
| "If it appears as though it returns a member of [?], it
| must hang/crash, because there are no members of [?]."
|
| 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?
| ackfoobar wrote:
| > a prover-adjacent language
|
| No need to go that far, you just need an ML inspired
| language with subtyping.
|
| https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-noth
| ing... 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
| bregma wrote:
| The word "function" in mathematics and the word
| "function" in C (and C++) are homonyms. Two words spelled
| the same and pronounced the same but with entirely
| different meanings. Any effort to conflate the two will
| just end in tragedy.
| Joker_vD wrote:
| Because you can't use void in templates uniformly: you can't
| have "T x;" when T = void, you can't do "return T{};", you
| can't form types like "(*R)(int, T)", etc.
| tpolzer wrote:
| What's really weird to me is not that C++ has a unit type and
| picked a weird name for it (that's just C++). The weird thing is
| how many unit types it has:
|
| - std::nullopt_t
|
| - std::nullptr_t
|
| - std::monostate
|
| - std::tuple<>
|
| And I'm sure there's more.
| bombela wrote:
| What about "void"?
| tines wrote:
| void isn't a unit type (inhabited by a single value), it's a
| "bot" type, I.e. no values inhabit it.
| jey wrote:
| And "bot" refers to the bottom type:
| https://en.wikipedia.org/wiki/Bottom_type
| gpderetta wrote:
| void is the unit type. The fact that it is not
| constructible is a wart of the language, inherited from C.
| It would be easy to fix and would simplify a significant
| amount of generic code.
|
| 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.
| tines wrote:
| That's a good point. Maybe one could argue that rather
| than the unit type not being constructible, the wart of C
| is that functions that return "bot" can still "finish
| executing without returning".
|
| 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;".
| MathMonkeyMan wrote:
| I ran into this recently writing some C++20 coroutines.
| The protocol for delivering values from a coroutine that
| was previously suspended has two flavors: one for values
| and one for void. My initial draft just implemented the
| value version and used a struct VoidTODO {} where void
| should be.
|
| 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.
| rerdavies wrote:
| Yes!! Been there done that. Two flavors of EVERYTHING:
| one to deal with functions that return values; and one to
| deal with void functions. It's awful.
| kazinator wrote:
| void is not a subtype of all types, though.
|
| 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.
| gpderetta wrote:
| > 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.
|
| 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...
| kazinator wrote:
| In C, it is not like that. From the 2023 draft:
|
| _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.
| omnicognate wrote:
| The distinct types are the whole point. You wouldn't want a
| std::tuple<> to be implicitly convertible to a std::optional<T>
| (for arbitrary T), and std::nullptr_t exists to be the type of
| nullptr, which captures the conversion behaviours appropriate
| for null pointer literals and has nothing to do with the
| variant use case std::monostate exists to serve.
| tpolzer wrote:
| If there was a std::unit_t and it was implicitly convertible
| to optional, tuple and pointer, I don't think that would be
| worse in terms of usability at all (maybe worse in
| readability for people who haven't heard of a 'unit' type).
|
| 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.
| omnicognate wrote:
| std::monostate is explicitly provided for use with
| std::variant. It's in the <variant> header. Sometimes
| people use it for other things, but that's really an abuse,
| especially given defining your own type suitable for such
| cases is typically as simple as `struct mytype{};`.
|
| 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?
| gumby wrote:
| Then you're basically back to "anything can convert back
| and forth with void _" -- the point is to _ avoid* that.
| ape4 wrote:
| Good thing they made this instead of expanding the standard
| library to be more like Java's or Python's. It still only
| contains the most basic functions, and std::monostate ;)
| leecommamichael wrote:
| Raymond is a very smart and productive person, and is not
| maligning C++ at all in this article. It makes me want to
| reassess my bittersweet perspective on the language.
| doctorpangloss wrote:
| On the flip side, it shows why so many people are excited about
| Rust. You can pick up literally any valuable thing off the
| ground that made the mistake of being built around C++, like
| CUDA, slap it on Rust, and people will both adopt it and be
| excited to contribute to it.
| furyofantares wrote:
| Gerard: But it doesn't _do_ anything!
|
| Hans: No -- it _does_ nothing.
|
| https://scryfall.com/card/wth/154/null-rod
| gpderetta wrote:
| No no, that's a void _pointer_ .
| nostrademons wrote:
| A "monostate" in the design-patterns lingo of 20 years ago was a
| class with only static member variables, basically where all
| state is shared state. It was supposed to be alternative to the
| Singleton pattern where you don't need all those .getInstance()
| calls and instead can just default-construct an instance, any
| instance, to get access to the shared state. It fell out of favor
| because usage was fairly error-prone and surprising to
| programmers who are not familiar with the pattern. Most people
| expect that when you create a new instance, you are actually
| creating new state, but monostate intentionally makes each new
| instance just a window on the shared global state.
|
| 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?
| quietbritishjim wrote:
| That just sounds like a different thing with the same name.
| It's nowhere close to the purpose of std::monostate.
| kazinator wrote:
| It's poor name. If it has no members, it holds no bits. Therefore
| it has no state. It's not enough for an object to exist in order
| to hold state. It must be capable of distinguishing at least
| between two values, like true or false. If something doesn't hold
| state, the word _state_ has no business in its name.
|
| 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.)
| binary132 wrote:
| "True" and "False" sound like two states. This is just one. :)
| kazinator wrote:
| Yes, exactly like 640K sounds like a power of two, if you're
| a MS-DOS user.
|
| 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.
| JackYoustra wrote:
| There's a fun thing like this in Swift too! `Void` is an empty
| tuple, and has all of the related constraints (can't conform to
| protocols, being the most salient one). If you have a type that
| has to conform to, say, `Equatable` or `Codable` you should
| instead use `Never?` which you can conform to most protocols via
| throwing `fatalError` on an extension of `Never` to the protocol.
|
| Anyway, I write basically this on my blog for a more thorough
| explanation: https://www.jackyoustra.com/blog/non-equatable-void
| shadowgovt wrote:
| So... Isn't the fact that you can't default-construct that
| variant without `monostate` working-as-intended?
|
| 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.
| vasilipupkin wrote:
| this is crazy confusing stuff that should be banned by most
| sensible internal coding standards.
___________________________________________________________________
(page generated 2024-07-22 23:08 UTC)