Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type. I'm starting a new thread for this paper specifically to get feedback on this proposal, which will be presented at Kona. If there are questions or criticisms of the idea, try to keep them in the context of this paper. In particular, I'd like to keep this discussion a bit more grounded than the other and more specific to precisely what is proposed here -- what might this proposal break (apart from what is already explicitly covered in the paper unless there is more to add), are there examples of problems that people expect to be solved by void that are not addressed here, do people see problems with any of the more subtle issues regarding the details of the proposed changes to the standard itself, etc.. I'm not strictly against tangents about void remaining a non-instantiable type, but if you are of that camp, please present your argument in the context of this paper and with real world examples, for instance how such an approach would apply to the logging examples presented in the paper, what problems, if any, such an approach might solve that this cannot, etc..[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0146r0.html
--
void
type, a pointer, at least in the traditional sense, would no longer be
able to be used as an iterator into that array (notably meaning that
generic code which relies on this would now fail for such a size 0
type)."On Mon, Oct 5, 2015 at 2:43 PM, Matt Calabrese <cala...@google.com> wrote:--Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type. I'm starting a new thread for this paper specifically to get feedback on this proposal, which will be presented at Kona. If there are questions or criticisms of the idea, try to keep them in the context of this paper. In particular, I'd like to keep this discussion a bit more grounded than the other and more specific to precisely what is proposed here -- what might this proposal break (apart from what is already explicitly covered in the paper unless there is more to add), are there examples of problems that people expect to be solved by void that are not addressed here, do people see problems with any of the more subtle issues regarding the details of the proposed changes to the standard itself, etc.. I'm not strictly against tangents about void remaining a non-instantiable type, but if you are of that camp, please present your argument in the context of this paper and with real world examples, for instance how such an approach would apply to the logging examples presented in the paper, what problems, if any, such an approach might solve that this cannot, etc..[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0146r0.html
"For instance, if you were to make an array of such avoid
type, a pointer, at least in the traditional sense, would no longer be able to be used as an iterator into that array (notably meaning that generic code which relies on this would now fail for such a size0
type)."You could still iterate. Since ptr++ would add 0 to the pointer, you would still always point to a/the void.What you can't do is take the distance. Or reach the end.
Anyhow, I think the main purpose of the proposal will be to just see how the committee reacts to the idea. I think you've done a good job at "it's not as big/scary as it sounds", but the reaction might still be "too scary", regardless of the details. I almost don't think there's much sense discussing it here until we get a feel from the committee as to whether ANY regularized-void idea has a chance.
Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type. I'm starting a new thread for this paper specifically to get feedback on this proposal, which will be presented at Kona. If there are questions or criticisms of the idea, try to keep them in the context of this paper. In particular, I'd like to keep this discussion a bit more grounded than the other and more specific to precisely what is proposed here -- what might this proposal break (apart from what is already explicitly covered in the paper unless there is more to add), are there examples of problems that people expect to be solved by void that are not addressed here, do people see problems with any of the more subtle issues regarding the details of the proposed changes to the standard itself, etc.. I'm not strictly against tangents about void remaining a non-instantiable type, but if you are of that camp, please present your argument in the context of this paper and with real world examples, for instance how such an approach would apply to the logging examples presented in the paper, what problems, if any, such an approach might solve that this cannot, etc..[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0146r0.html
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.
Right, but in other places where you have infinite ranges, whether or not the range is infinite does not usually simply depend on the element type. That makes things really difficult. The main concern is code like the following://////////template<class T>void foo(){T bar[10];for(T* curr = bar, end = curr + 10; curr != end; ++curr) {}}//////////For most Ts (all T's that can appear as an array element, currently) this would iterate over each element. For void, if you make it size 0, this would be an infinite loop.
On Mon, Oct 5, 2015 at 12:37 PM, Tony V E <tvan...@gmail.com> wrote:On Mon, Oct 5, 2015 at 2:43 PM, Matt Calabrese <cala...@google.com> wrote:--Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type. I'm starting a new thread for this paper specifically to get feedback on this proposal, which will be presented at Kona. If there are questions or criticisms of the idea, try to keep them in the context of this paper. In particular, I'd like to keep this discussion a bit more grounded than the other and more specific to precisely what is proposed here -- what might this proposal break (apart from what is already explicitly covered in the paper unless there is more to add), are there examples of problems that people expect to be solved by void that are not addressed here, do people see problems with any of the more subtle issues regarding the details of the proposed changes to the standard itself, etc.. I'm not strictly against tangents about void remaining a non-instantiable type, but if you are of that camp, please present your argument in the context of this paper and with real world examples, for instance how such an approach would apply to the logging examples presented in the paper, what problems, if any, such an approach might solve that this cannot, etc..[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0146r0.html
"For instance, if you were to make an array of such avoid
type, a pointer, at least in the traditional sense, would no longer be able to be used as an iterator into that array (notably meaning that generic code which relies on this would now fail for such a size0
type)."You could still iterate. Since ptr++ would add 0 to the pointer, you would still always point to a/the void.What you can't do is take the distance. Or reach the end.Right, but in other places where you have infinite ranges, whether or not the range is infinite does not usually simply depend on the element type. That makes things really difficult. The main concern is code like the following://////////template<class T>void foo(){T bar[10];for(T* curr = bar, end = curr + 10; curr != end; ++curr) {}}//////////For most Ts (all T's that can appear as an array element, currently) this would iterate over each element. For void, if you make it size 0, this would be an infinite loop. In places where void comes up as a dependent type, you shouldn't be expected to special-case. It should just work. In this case you could use range-based-for and the standard could have std::begin/std::end yield something other than a pointer, but that is only a partial solution.My personal thoughts are that, at a higher level (outside of the scope of this paper) we shouldn't assume that pointers are valid array iterators, which would break down for size 0 types.
I'm not about to make this proposal try to tackle this and other issues since these problems are somewhat orthogonal. Sean really wants size 0 void (and size 0 other types), and I do as well, I just don't think it's feasible at this point in time as a part of this proposal. It can still happen, but solving that problem is much more difficult and there are many more subtle issues that come from size 0 types regarding assumptions people are allowed to make in current code (that type + address is unique for each object, for example). Leaving void just unspecified on size for now (but still >= 1 as other types) is fine, I think, and if we ever get size 0 types, I'd expect implementations to make void be one of those types.
You specifically mention possibly re-introducing the int "foo(void)" syntax but note that it may create problems because it isn't an unary function. Now I think this can be solved with three adjustments, let me see if I missed anything:
(1) Let any function declared with no formal parameters "int foo()" be a shortcut for and implicitly identical to "int foo(void)".
(2) Let any function that only takes a single formal parameter of type void be callable with an empty argument list.
(3) When matching template arguments an explicit overload/specialization of "int()" binds stronger than "int(T)" (where T <- void)
Does this make sense?
Pointers not being valid iterators breaks pretty much the world at this point. At the very least, you'd royally honk-off SG14 if you suddenly declare that std::vector and std::array cannot use pointers for their iterators.
Well, when I was working through the issues with defining stateless types (which are guaranteed to not take up space as members or base-classes of a struct), I used the general rule that the type should work like regular types as much as possible. They would have a non-zero size, for example, so you could legitimately allocate them dynamically on the heap if you had some reason to do so.
I ran into a fundamental problem with arrays through. At some point, you have to violate something. These were the options:
1) stateless members that are arrayed take up space (thus sometimes violating the "don't take up space" principle)
2) stateless members that are arrayed all have the same address (thus violating the rules of pointer arithmetic and arrays)
3) stateless members cannot be arrayed (thus violating the rule of behaving like regular types)
4) stateless types actually have sizeof return 0 (thus violating... well, all kinds of things)
I considered #3 to be the less onerous solution. We both seemed to come to the same conclusion on that one.
I should have maybe mentioned this in the proposal. I suggested a solution sort of similar to this in the middle of the long thread "Allow values of void," but I've since decided it's somewhat questionable (I further suggested generalizing it so that any "empty" argument, regardless of the location in the argument list, can be considered to be equivalent to doing void{}, making something like foo(5, 'a', , 3.0) valid, with the third argument being void{}). The difference between what you are suggesting and what I was suggesting is that I still think that we'd need to keep the function types separate between int foo(void) and int foo(). If we didn't, we could have really subtle differences between void and other types here that can still break generic code. With the function types being equal, for instance, if you had both of those declarations you'd now be producing two overloads for all types T except void, but for void it would re-declare the same function and can cause other problems, such as if those declarations were both definitions. When void is a dependent type there, this becomes particularly subtle and can require special-casing. As well, if you agree with my rationale for why the function declarations would need to be separate types, then this also now implies that existing code is potentially broken, since right now it is allowable to do: void foo(); void foo(void) {} and the definition is the definition of the declared function. If we changed this, it means that any existing code that does this would now likely break. This can really be bad if the code that does this is a part of a compiled dependency.
In addition to that, the other reason I strayed from this type of approach is that it requires an altered kind of overload set to be considered when there is a single argument of type void. So there is more special-casing on the implementation side, and potentially this needs to be understood by callers to avoid subtleties. Ultimately, the language would be simpler without this rule, and I think if we designed the language from scratch we wouldn't have such a rule, so we should ideally try to more directly get what we want, only accounting for compatibility in the places where it is strictly necessary. I don't think we gain much by trying to do this sort of thing, but we definitely make the language more complex and add some subtleties that are special to void.
I still think trying to solve the problem that templates only
handle the one-value case by removing the zero case from the existing
zero-or-one that is presently allowed is the wrong solution (and wrong
direction, i.e. a regression rather than progress), and we should
instead be working towards zero-or-many support. Languages with first
class multiple return values and language level multi-value support
(e.g. unpacking) do much better in this realm.
- How do we deal with the implications of claiming that void is a
regular type, when a void function *has no return value* (keep in mind
this may have ABI and/or register allocation implications).
- If at some point we add first class multiple return values from
functions, how will we reconcile that 'void foo()' returns *zero* values
and not one value? Related, how do we explain away 'return;' as a
zero-value return in a function whose return type is specified as
"void", especially in the face of syntax to actually return multiple values?
I don't see in the changes how you reconcile claiming that void is a
regular type with the fact that a void function *doesn't return a
value*, or for that matter how 'void foo() { return; }' is still
supposed to work.
Am 05.10.2015 um 23:39 schrieb 'Matt Calabrese' via ISO C++ Standard - Future Proposals:
Hmm maybe I didn't get this over well enough. In my approach "void foo(); void foo(void) { }" defines the *same* function. There is no breaking here. My implication is that we treat any function that is declared/defined as "void foo()" as if it were declared/defined as "void foo(void)". The two refer to the same entity and are interchangeable. That alone would break existing code at the call site, unless we also say that calling a "void foo(void)" function as "foo()" is equivalent to "foo(void{})".
I should have maybe mentioned this in the proposal. I suggested a solution sort of similar to this in the middle of the long thread "Allow values of void," but I've since decided it's somewhat questionable (I further suggested generalizing it so that any "empty" argument, regardless of the location in the argument list, can be considered to be equivalent to doing void{}, making something like foo(5, 'a', , 3.0) valid, with the third argument being void{}). The difference between what you are suggesting and what I was suggesting is that I still think that we'd need to keep the function types separate between int foo(void) and int foo(). If we didn't, we could have really subtle differences between void and other types here that can still break generic code. With the function types being equal, for instance, if you had both of those declarations you'd now be producing two overloads for all types T except void, but for void it would re-declare the same function and can cause other problems, such as if those declarations were both definitions. When void is a dependent type there, this becomes particularly subtle and can require special-casing. As well, if you agree with my rationale for why the function declarations would need to be separate types, then this also now implies that existing code is potentially broken, since right now it is allowable to do: void foo(); void foo(void) {} and the definition is the definition of the declared function. If we changed this, it means that any existing code that does this would now likely break. This can really be bad if the code that does this is a part of a compiled dependency.
The (I think) only point where treating "void()" and "void(void)" as identical signatures would break is in in template argument matching:
template<class R>
void foo(std::function<R()> f);
template<class R, class T>
void foo(std::function<R(T)> f);
Here we either have to specify that the "R()" overload binds stronger than "R(T)" (where T=void), or that the signature "R()" is a shorthand for "R(void)" (as it is in function decls/defs), because that is how existing code works. I prefer the latter option. New code would ideally no longer require the "R()" overload at all.
On 06 Oct 2015, at 01:10 , 'Matt Calabrese' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:On Mon, Oct 5, 2015 at 3:07 PM, Miro Knejp <miro....@gmail.com> wrote:Am 05.10.2015 um 23:39 schrieb 'Matt Calabrese' via ISO C++ Standard - Future Proposals:
Hmm maybe I didn't get this over well enough. In my approach "void foo(); void foo(void) { }" defines the *same* function. There is no breaking here. My implication is that we treat any function that is declared/defined as "void foo()" as if it were declared/defined as "void foo(void)". The two refer to the same entity and are interchangeable. That alone would break existing code at the call site, unless we also say that calling a "void foo(void)" function as "foo()" is equivalent to "foo(void{})".
I should have maybe mentioned this in the proposal. I suggested a solution sort of similar to this in the middle of the long thread "Allow values of void," but I've since decided it's somewhat questionable (I further suggested generalizing it so that any "empty" argument, regardless of the location in the argument list, can be considered to be equivalent to doing void{}, making something like foo(5, 'a', , 3.0) valid, with the third argument being void{}). The difference between what you are suggesting and what I was suggesting is that I still think that we'd need to keep the function types separate between int foo(void) and int foo(). If we didn't, we could have really subtle differences between void and other types here that can still break generic code. With the function types being equal, for instance, if you had both of those declarations you'd now be producing two overloads for all types T except void, but for void it would re-declare the same function and can cause other problems, such as if those declarations were both definitions. When void is a dependent type there, this becomes particularly subtle and can require special-casing. As well, if you agree with my rationale for why the function declarations would need to be separate types, then this also now implies that existing code is potentially broken, since right now it is allowable to do: void foo(); void foo(void) {} and the definition is the definition of the declared function. If we changed this, it means that any existing code that does this would now likely break. This can really be bad if the code that does this is a part of a compiled dependency.Right, I think I understand what you're saying, but even this has implications in generic code. Consider the following://////////template<class T>struct foo{foo() { /**/ } // default constructorfoo(T init) : bar(std::move(init)) {}T bar;};//////////
Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type.
// Invoke a callable, logging its arguments and return value.
template<class R, class... P, class Callable>
R invoke_and_log(callable_log<R(P...)>& log, Callable&& callable, P... args) {
log.log_arguments(args...);
auto result = std::invoke(std::forward<Callable>(callable),
std::forward<P>(args)...);
log.log_result(result);
return result;
}
// Invoke a callable, logging its arguments and return value.
template<class R, class... P, class Callable>
R invoke_and_log(callable_log<R(P...)>& log, Callable&& callable, P... args) {
log.log_arguments(args...);
return log.log_result(
std::invoke(std::forward<Callable>(callable),
std::forward<P>(args)...));
}
On 2015-10-05 18:53, Matt Calabrese wrote:
> Further, even if we had a language-level solution for returning N
> different values, you'd still want to be able to pass these entities
> around as a single object *without* unpacking them as separate
> arguments, otherwise examples like my second logging example (the one
> with the verbosity option) break down.
True, and that's similarly why I think that something like:
void foo();
auto x = foo();
...should be supported in any case. But *not* by making void a regular
type. There should be just as much difference between assigning to a
single variable from a 0-value function vs. a 1-value function as from
an N-value function vs. a 1-value function (for N > 1).
For what it's worth, I was strongly in favor of being able to treat
MRV's as a single object. There was also, however, some strong opposition.
> Even in this case, I'm not certain that the implication is that
> "void" would or should be equivalent to the "tuple with no elements"
> type.
I would find it very strange for it to mean otherwise, when this is what
it has meant historically.
Incidentally, I'm sure we would want some way to force a 1-value result
to be "packed" the same as an N-value result. We can't always pack it
for the sake of compatibility, but generic code would need a way to
ensure consistency.
On Mon, Oct 5, 2015 at 8:43 PM, Matt Calabrese <cala...@google.com> wrote:Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type.I've thought this through a couple of times before.I think void should represent the absence of type, rather than be a type that holds one value. It is correctly, imho, incomplete.
I understand the extra work that has to be done occasionally in generic programming where you have to write a separate specialization to deal with void (and have done this personally more than once).I do not think the time saved by this rare case, outweighs the time lost from either migration to a "regular" void, or the end result of the strange cases that come up:void p;void* q = &p; // void* has special propertiesvoid A[10];std::vector<void> v;void f(void); // 0 or 1 parametersizeof(void)SFINAE uses of voidetc
I find this paper to be an interesting examination of the problem. It attempts to make `void` as regular as one could reasonably expect such a type to be.
However, the question is this: do we want `void` to be regular, or do we want to avoid having to specialize templates for it in so many cases?
If the goal is to make `void` regular, then P0146 obviously fails. There are too many occurrences of `void` being irregular for users to be able to generally rely on type regularity.
I think that making `void` regular is simply the wrong goal. I really think the goal should be to minimize the number of times you need to specialize templates.
However, this particular implementation of return value logging introduces an unnecessary construct: a named object of the return type. You could instead implement it like this:
// Invoke a callable, logging its arguments and return value.
template<class R, class... P, class Callable>
R invoke_and_log(callable_log<R(P...)>& log, Callable&& callable, P... args) {
log.log_arguments(args...);
return log.log_result(
std::invoke(std::forward<Callable>(callable),
std::forward<P>(args)...));
}
2) The ability to pass the result of a function to another function, even if that result is `void`. In the `void` case, the argument should effectively disappear.
By examining the problem domain like this, I think you can find ways to avoid some of the more pernicious issues of `void` regularity. I'd guess you can avoid having named values of `void` type. Which also means you can avoid questions of getting references to `void`, passing them around as arguments, copy/move, and so forth.
On 2015-10-06 14:26, Matt Calabrese wrote:
> If you personally decided that you didn't like this functionality due
> to religious reasons, just don't use those functions.
It doesn't work that way. My concern is that if your proposal is
accepted, we lose the ability to express certain concepts that we can
currently express
(i.e. void as not-a-type), and where, if (hopefully
when) we later want to add "real MRV support", we've painted ourselves
into a corner where we've lost the ability to use 'void' for 0-tuples as
it seems we would want to so do.
The problem isn't that it will break my
legacy code, it's that it moves the language in a questionable direction
from which it is difficult to backtrack.
I will, however, refrain from discussing the other questions you asked,
which are indeed drifting quite far off topic. (I will say, however,
briefly, that one of the major arguments for language-level tuples /
MRV's is to improve generic programming abilities in the face of N-tuple
returns as well as unpacking the same as function arguments. The most
important argument however is RVO.)
The level of contention on the list seems to have killed anything
getting further.
> On Tue, Oct 6, 2015 at 7:14 AM, Matthew Woehlke wrote:
>> I would find it very strange for [a void function to mean something
>> other than returning a 0-tuple, which] is what it has meant
>> historically.
>
> Has it?
Yes. See in particular the lack-of-return equivalence that mentions the
lack of a value, the general way in which the result of a void function
disappears and/or cannot be used, and the machine instructions emitted
in a void function.
A non-void function has a return slot that is filled
with the function result. A void function has no return slot and nothing
is filled on return. If this doesn't describe the difference between a
1-tuple return and a 0-tuple return, I don't know what does.
That's *exactly* what I *would* expect. 1-tuple returns should be
interchangeable with "traditional non-void returns" (absent syntax to
force interpretation as a tuple). Likewise, 0-tuple returns should be
interchangeable with "traditional void returns".
Otherwise, we might as well forget all about compatibility with legacy
code and start over from scratch.
The reason this is relevant is because the direction of your proposal is
opposed to this.
if a monostate already
behaves exactly like you want void to behave, why do we need to change
anything? Just teach people to use std::monostate instead of void, and
problem solved.
which is the categorical dual of the top or unit type.
On 5 October 2015 at 13:43, Matt Calabrese <cala...@google.com> wrote:Given that void is all over the place in the C standard, which is the base document for C++, it is surprising not to find a detailed section on C Compatibility in your proposal. Do you plan on adding one soon?
--Nevin ":-)" Liber <mailto:ne...@eviloverlord.com> (847) 691-1404
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
On Tue, Oct 6, 2015 at 9:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:I find this paper to be an interesting examination of the problem. It attempts to make `void` as regular as one could reasonably expect such a type to be.
However, the question is this: do we want `void` to be regular, or do we want to avoid having to specialize templates for it in so many cases?
If the goal is to make `void` regular, then P0146 obviously fails. There are too many occurrences of `void` being irregular for users to be able to generally rely on type regularity.Why do you think void is not Regular in this proposal? To be clear, "Regular" is with respect to generic programming (there is a paper linked in the references that describes Regular types, but they are also described in Stepanov's books, formally in the old C++0x proposal, and I'm sure the current Concepts TS will have such a concept if it doesn't specify one already). In brief, a Regular type just has proper copy/move semantics is comparable, and move is noexcept. This proposal makes void Regular, and it is simply a monostate type.
On Tue, Oct 6, 2015 at 9:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:I think that making `void` regular is simply the wrong goal. I really think the goal should be to minimize the number of times you need to specialize templates.This is precisely how you accomplish that goal in a way that doesn't leave void as some kind of special type that needs explicit consideration in generic code. Treating void as special is exactly the problem. It doesn't have to be anything special. It can just be a Regular type and you're not losing anything by that. Being special in the sense of not supporting fundamental operations is what forces users to write code in odd ways and to special case.
// Invoke a callable, logging its arguments and return value.
template<class R, class... P, class Callable>
R invoke_and_log(callable_log<R(P...)>& log, Callable&& callable, P... args) {
log.log_arguments(args...);
constexpr_if(is_same_v<void, std::result_of_t<Callable, Args...>>)
{
auto result = std::invoke(std::forward<Callable>(callable),
std::forward<P>(args)...);
log.log_result(result);
return result;
}
else
return;
}
On Tue, Oct 6, 2015 at 9:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:By examining the problem domain like this, I think you can find ways to avoid some of the more pernicious issues of `void` regularity. I'd guess you can avoid having named values of `void` type. Which also means you can avoid questions of getting references to `void`, passing them around as arguments, copy/move, and so forth.You are still forcing writers of generic code to have arbitrary considerations if they want to work with void. This also doesn't avoid questions like getting references to void, or forming arrays, or anything at all. These still frequently come up in generic code.
My issue here is that you seem to be conflating two points, using one to hide your desire for the other. Your proposal is very clear on the motivations. It's not that `void` ought to be a type, for some functional programming reason or whatever.
The motivation is because, by making `void` regular, you make template programming easier in various situations. You don't have to make complex specializations and such just for a minor case of a function with no return value.
However, if the goal is making template code easier to write, then whether `void` is "some kind of special type that needs explicit consideration in generic code" is irrelevant. What matters is how hard it is to provide that "explicit consideration".
Given that C does not allow function overloads by name, and has no templates, am I wrong in thinking that the effects are essentially non-existent?
On 5 October 2015 at 13:43, Matt Calabrese <cala...@google.com> wrote:Given that void is all over the place in the C standard, which is the base document for C++, it is surprising not to find a detailed section on C Compatibility in your proposal. Do you plan on adding one soon?
On Tuesday, October 6, 2015 at 3:10:09 PM UTC-4, Matt Calabrese wrote:On Tue, Oct 6, 2015 at 9:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:I find this paper to be an interesting examination of the problem. It attempts to make `void` as regular as one could reasonably expect such a type to be.
However, the question is this: do we want `void` to be regular, or do we want to avoid having to specialize templates for it in so many cases?
If the goal is to make `void` regular, then P0146 obviously fails. There are too many occurrences of `void` being irregular for users to be able to generally rely on type regularity.Why do you think void is not Regular in this proposal? To be clear, "Regular" is with respect to generic programming (there is a paper linked in the references that describes Regular types, but they are also described in Stepanov's books, formally in the old C++0x proposal, and I'm sure the current Concepts TS will have such a concept if it doesn't specify one already). In brief, a Regular type just has proper copy/move semantics is comparable, and move is noexcept. This proposal makes void Regular, and it is simply a monostate type.
I seem to have read your proposal backwards then. I was looking for the treatment of specific instances, so my searches all lead to the "alternative" section, which I mistook for the actual proposal.
On Tue, Oct 6, 2015 at 9:06 AM, Nicol Bolas <jmck...@gmail.com> wrote:I think that making `void` regular is simply the wrong goal. I really think the goal should be to minimize the number of times you need to specialize templates.This is precisely how you accomplish that goal in a way that doesn't leave void as some kind of special type that needs explicit consideration in generic code. Treating void as special is exactly the problem. It doesn't have to be anything special. It can just be a Regular type and you're not losing anything by that. Being special in the sense of not supporting fundamental operations is what forces users to write code in odd ways and to special case.
Wait: if the goal is to help improve template code writing, why does it matter if `void` is a special type or not?
The motivation is because, by making `void` regular, you make template programming easier in various situations. You don't have to make complex specializations and such just for a minor case of a function with no return value.
However, if the goal is making template code easier to write, then whether `void` is "some kind of special type that needs explicit consideration in generic code" is irrelevant. What matters is how hard it is to provide that "explicit consideration".
For example, P0128 (consexpr_if) solves your logging problem thusly:
// Invoke a callable, logging its arguments and return value.
template<class R, class... P, class Callable>
R invoke_and_log(callable_log<R(P...)>& log, Callable&& callable, P... args) {
log.log_arguments(args...);
constexpr_if(is_same_v<void, std::result_of_t<Callable, Args...>>)
{
auto result = std::invoke(std::forward<Callable>(callable),
std::forward<P>(args)...);
log.log_result(result);
return result;
}
else
return;
}This code looks perfectly reasonable. You could even write a metafunction to make the conditional a bit shorter, if that bothers you.
This is a solution to the problem that motivates your proposal. You probably won't prefer this solution. But you cannot deny that it substantially reduces the burden on users when doing this sort of stuff. It doesn't require adding function specializations and whatnot. It just works, and its easy to read and reason about.
There is another problem with regularizing `void` that your proposal does not deal with. Namely, that you've removed a fundamental distinction between "functions that return a value" and "functions that don't return a value".
As an example of where this distinction is important, consider a specialized transformation visitor for a variant. That is, it converts one variant into another variant. And let's say we're clever and we decree that the typelist for the generated variant will be deduced from the return types of the visitor (I don't know if that's possible, but it sounds plausible).
Well, answer me this: what would it mean for the user to provide a `void` function in such a visitor? Did the user make a mistake? Or does the user actually mean to store the `void` value?
It would make far more sense for such a function to reject `void` functions. Why? Because thanks to auto return type deduction, it's very easy to compute a value and forget to return it, thus accidentally creating a `void` function. And sure, a compiler might offer a warning, but a compiler error is much more effective. If the user truly wanted an "empty" state, they'd use an empty class.
In short, not every user wants to treat `void` as regular. Why should regularity be the default case?
So on the one hand, your solution to the problem seems like overkill; we can solve the problem adequately without affecting `void`.
You would strengthen your motivation section if you could provide real-world example code of cases where these operations are necessary. It would also make it easy for a user to judge if the code is logically consistent.
It seems to me that most instances of such occurrences are when dealing with return values of user-provided functions. And thus, most such operations would naturally not make sense when dealing with user-provided functions that have no return value. So code that builds an array of return values from functions that don't have a return value seems... odd. That sounds like the user provided the wrong function, which should be a compiler error.
--
I think I can help strengthen the motivational case for this feature (despite the fact that I loathe the very concept of it).
I think the logging function is a terrible example, and it should not be the primary focus of the motivation section. Though it is a simple example, it is precisely the simplicity that's the problem. It's too easily dismissed, for three reasons:
1) The required code to support functions with no return value is, quite frankly, not that hard to write.
2) There are proposed language features that make it even easier to both write and read.
3) It raises the question of why a user would want to log `void` returns at all. The function call and it's parameters, yes. But does a user really want to see a bunch of "void"s in the log file? (FYI: in one of my earlier posts, I did forget the `invoke` call by mistake. But the lack of logging in the `void` case was very much deliberate.)
std::promise and std::future
These are pretty much unbreakable. The `future` type, by its very nature, must have storage for its value. But, if it doesn't have a value... what does it store? Which means that you need to change more than just a function or two.
You have to write the class all over again. Which is of course what the standard requires.
Oh sure, you can derive from a common base that contains the work not related to the value (and you'll be doing that anyway, since you have to support `R&`). But you have to write four sets of constructors (1 common base, 3 for the various specializations). You have to write three separate implementations of your getter function. And so forth.
Something similar goes for `promise`.
While the solution of a common base may mitigate some problems, it makes maintaining the code more complex. If the interface needs to grow, then you may need to implement this in 3 cases. You have to test your code against 3 possibilities. And so forth.
At the end of the day, it's a mess. And it's a mess that no language feature can easily work around. Concepts won't help. `constexpr_if` won't help. You just have to bite down.
Examples like those are your best bet here. It almost convinced me that it's a good idea.
Following the "Allow values of void" discussion, I've put together a proposal[1] for updating void to be a Regular type.