I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.This idea struck me to solve both of these issues at once by a few small changes:1. Allow zero sized arrays in C++.No code would break by just allowing something previously forbidden. The problem with allowing this could have been to make up ones mind about what size such an array should report.
I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.This idea struck me to solve both of these issues at once by a few small changes:1. Allow zero sized arrays in C++.No code would break by just allowing something previously forbidden. The problem with allowing this could have been to make up ones mind about what size such an array should report.
2. Define that a zero size array occupies zero bytes.
I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.This idea struck me to solve both of these issues at once by a few small changes:1. Allow zero sized arrays in C++.No code would break by just allowing something previously forbidden. The problem with allowing this could have been to make up ones mind about what size such an array should report.
2. Define that a zero size array occupies zero bytes.As zero size arrays have never been allowed in C++ it can't be a breaking change to define that the now allowed zero size arrays actually consume no bytes regardless of the size of the element.With these two rules in place it is possible to write this:// Example container class.template<typename T, typename A = std::allocator<T>> MyContainer {public:A& GetAllocator() { *m_allocator; } // Access the only element or if none return some other address (next member) that the caller will not use anyway as it points to 0 bytes of information.private:A m_allocator[std::is_empty<A> ? 0 : 1];};To be able to wrap the ugly ? operator and give the construct a recognizable name some more provisions are needed, this is as if you wrap the possibly empty array into a class that class is not a zero size array and thusby the current rules must occupy at least one byte.
On Monday, July 4, 2016 at 11:15:15 AM UTC-4, Bengt Gustafsson wrote:I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.This idea struck me to solve both of these issues at once by a few small changes:1. Allow zero sized arrays in C++.No code would break by just allowing something previously forbidden. The problem with allowing this could have been to make up ones mind about what size such an array should report.
Allow them to do... what?
2. Define that a zero size array occupies zero bytes.
The C++ object model requires that objects take up storage. If you have an array that has no objects in it, then you clearly cannot use objects that don't exist. So having an array of 0 objects means that you can't do anything with `a[0]`.
Merely permitting the syntax doesn't work. You have to actually define what that means. Whether it's a zero-sized array or an object that takes no storage or whatever, you still need to do the hard work of deciding what that actually means.
And for a zero-sized array, you also have to decide what it means for the array to decay to a pointer.
How we declare that a variable takes up no storage is not the hard part of having stateless members. Whether it's a magic standard library type, a zero-size array, or a new keyword, that's the easy part. The hard part is deciding what they mean, how pointers to them work, how they get copied, etc.
And you haven't really done any of that work.
One of the (current) SFINAE rules is that attempting to create an array of size zero causes the template instantiation to fail.This forces the compiler to consider other possible candidates.
Allowing zero sized arrays would cause the instantiation to succeed, which will obviously change the behaviour of existing programs.
Tobias
I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.This idea struck me to solve both of these issues at once by a few small changes:1. Allow zero sized arrays in C++.No code would break by just allowing something previously forbidden. The problem with allowing this could have been to make up ones mind about what size such an array should report.2. Define that a zero size array occupies zero bytes.As zero size arrays have never been allowed in C++ it can't be a breaking change to define that the now allowed zero size arrays actually consume no bytes regardless of the size of the element.
--
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.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CALnjya-8Kn7VR3J0yxfPoR%2BzpDGDNxXSUwh4ZpD-t1KdbN9Prw%40mail.gmail.com.
On 6 Jul 2016, at 13:06, Mathias Gaunard <mat...@gaunard.com> wrote:So you wantstruct Foo{char array[0];int value;};to be well-formed?
That's problematic for a number of reasons.Could you elaborate?
I see no problem with defining the meanings you are after here. Use logic. See also below.
Den måndag 4 juli 2016 kl. 18:30:24 UTC+2 skrev Nicol Bolas:
On Monday, July 4, 2016 at 11:15:15 AM UTC-4, Bengt Gustafsson wrote:I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.This idea struck me to solve both of these issues at once by a few small changes:1. Allow zero sized arrays in C++.No code would break by just allowing something previously forbidden. The problem with allowing this could have been to make up ones mind about what size such an array should report.
Allow them to do... what?Be declared. Have their address taken. Said address being being that of the first addressable byte after the previous variable in the scope.
struct Foo
{
int f;
float g[0];
};
Foo foo;
int j;
From this follows that begin() and end() on zero sized arrays both return this address, so that range based for loops 0 times.2. Define that a zero size array occupies zero bytes.
The C++ object model requires that objects take up storage. If you have an array that has no objects in it, then you clearly cannot use objects that don't exist. So having an array of 0 objects means that you can't do anything with `a[0]`.Of course not, just as with an array sized 1 you can't do anything with a[1], because there is nothing there.Of course any writing in the standard document mentioning that all C++ objects must take up storage must be amended with a clause that excepts zero sized arrays.
Merely permitting the syntax doesn't work. You have to actually define what that means. Whether it's a zero-sized array or an object that takes no storage or whatever, you still need to do the hard work of deciding what that actually means.I think all that is needed is stated above.
And for a zero-sized array, you also have to decide what it means for the array to decay to a pointer.It decays to its address as defined above. At this address there is no valid object, just as there isnt when you do &a[1] for a 1-long array today. I fail to see a problem here.
How we declare that a variable takes up no storage is not the hard part of having stateless members. Whether it's a magic standard library type, a zero-size array, or a new keyword, that's the easy part. The hard part is deciding what they mean, how pointers to them work, how they get copied, etc.An advantage of using the zero-sized array idiom is that the question of "how do I iterate over an array of zero sized objects" does not come up as a separate issue.
struct Bar
{
float f[20][0];
};I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.
On Mon, Jul 4, 2016 at 8:15 AM, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.When I started working on Regular Void (p0146) I initially explored size 0 types but decided to postpone the effort because it has some difficult practical concerns regarding backwards compatibility. I think it might be possible that we will eventually get to a point where the language allows size 0 types, but it is not something that we can just start introducing (arrays or otherwise) without broader considerations. If the language had size 0 types to begin with then there would be no potential issues, but I half-suspect that the transition to a world where size 0 types exist may be more difficult than people would care to accept.Here's a brief summary of my current conclusions. I'd like to eventually explore this again once Regular Void either goes into the language or is killed.1) In order for size 0 types to be useful as a means of minimizing storage, we'd effectively have to change aliasing rules. No two objects of the same type can occupy the same address in C++ as it currently is, which user-level code can and does exploit.
We'd want size 0 objects to able to [effectively] share an address. This is true even if you only limit size 0 types to arrays of size 0.2) The more important concern, IMO, is that size 0 types fundamentally break our assumption that raw pointer types are able to act as iterators into arrays. Again, this problem still exists and needs to be... addressed... (heh) even if you only allow arrays of size 0 and not more general size 0 types. A simple example of this is as follows:// This iterates 5 timesint a[5] = {};for( auto& elem : a ){// ...do whatever...}// This iterates 0 timesusing foo = int[0];foo b[5] = {};for( auto& elem : b ){// ...do whatever...}The above might seem contrived, but it's not at all. Once you allow size 0 types, they can easily turn up as dependent types in generic code for which arrays can be made. IMO, this issue is a symptom of the language incorrectly conflating an address and an iterator. For example, in a language with size 0 types, the above code could be made to work perfectly fine if it considered an iterator into an array whose element type is size 0 to be something other than a pointer (i.e. simply an integer value corresponding to the array index). Once that separation is made, you can go back to easily iterating over the arrays no matter what the element size is, but the underlying implication is that any generic code in C++ today that assumes a pointer is a valid iterator into an array would now be broken for empty types. That might not be a huge deal if you don't allow "struct foo {};" to be a size 0 type, but that also rules out one of the main reasons that people want size 0 types to begin with. Allowing size 0 arrays might seem like an easier start, but I think you technically have to solve all of the same problems anyway, even though those problems happen to be less-likely to come up in practice.Ultimately, I think that if we get size 0 types, we wouldn't just special case arrays of size 0. An array is just an object and it being size 0 requires the same considerations as other types being size 0. In the end, if we allow size 0 types, my personal conclusion is that:1) It shouldn't be restricted to size 0 arrays2) Different objects of the same (empty) type can share the same address3) std::array_iterator< /*array type*/ > or something similar yields an appropriate iterator type for the array (isn't a pointer type when the element type is size 0)4) [Unfortunately] for practicality, user-defined types should at least initially have some kind of specifier-hint that tells the implementation that the type is allowed to be size 0 if it happens to have no members or all of the members/bases are also size 0. This is subtle, but implementations all of a sudden having something like "struct foo {};" become size 0 overnight would never happen, and the same goes for struct foo { /*empty type members*/ };). Because of that, at least initially, people would have to explicitly request the ability for their type to potentially be size 0 when all other conditions allow it, otherwise they wouldn't be able to take advantage of it in practice during the transition.
In other words:struct /*some specifier*/ foo { /*potentially some datamembers which may or may not be empty*/ };where the existence of /*some specifier*/ allows the implementation to let "foo" be size 0.
On Thursday, July 7, 2016 at 8:09:37 PM UTC-4, Matt Calabrese wrote:On Mon, Jul 4, 2016 at 8:15 AM, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.When I started working on Regular Void (p0146) I initially explored size 0 types but decided to postpone the effort because it has some difficult practical concerns regarding backwards compatibility. I think it might be possible that we will eventually get to a point where the language allows size 0 types, but it is not something that we can just start introducing (arrays or otherwise) without broader considerations. If the language had size 0 types to begin with then there would be no potential issues, but I half-suspect that the transition to a world where size 0 types exist may be more difficult than people would care to accept.Here's a brief summary of my current conclusions. I'd like to eventually explore this again once Regular Void either goes into the language or is killed.1) In order for size 0 types to be useful as a means of minimizing storage, we'd effectively have to change aliasing rules. No two objects of the same type can occupy the same address in C++ as it currently is, which user-level code can and does exploit.
Actually that's not quite true, and it is this exception that focused my stateless subobject idea (that I'm still occasionally refining). There are certain cases where two objects can have the same address. Namely, base class subobjects and their derived class. This is why [basic.lval]/10 has an explicit exception in the aliasing rules for types which are "similar to" the actual object's type.
I would say that the requirement should be stronger than that. Your idea is to make it a hint, that implementations may allow the size to be 0. I say that it should be enforced. If you declare that a type is zero-sized, then put things in it that aren't themselves zero-sized, then I would say that your code is incoherent and does not deserve to compile. It seems likely to me that a user who declares that a type will be zero-sized, then does something that makes this impossible has made a mistake.
As I've refined my own stateless types idea, I came to realize something. While it is useful to be able to declare that a type will be stateless, it is also useful to declare that a type which was not explicitly declared stateless can be used as a stateless subobject. That way, you don't create a rigid separation between the world of stateless things and the world of non-stateless things.
If someone passes you an allocator that is an empty type, but they didn't declare it stateless, that's OK: you can fix that at the point of use.
On Thu, Jul 7, 2016 at 7:21 PM, Nicol Bolas <jmck...@gmail.com> wrote:On Thursday, July 7, 2016 at 8:09:37 PM UTC-4, Matt Calabrese wrote:On Mon, Jul 4, 2016 at 8:15 AM, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:I have been following the recent thread about empty base optimization, or rather, the lack of empty member optimization possibility. And today I again fell in the 0 sized array trap in metaprogramming, noticing that it creates quite a few problems requiring extra coding.When I started working on Regular Void (p0146) I initially explored size 0 types but decided to postpone the effort because it has some difficult practical concerns regarding backwards compatibility. I think it might be possible that we will eventually get to a point where the language allows size 0 types, but it is not something that we can just start introducing (arrays or otherwise) without broader considerations. If the language had size 0 types to begin with then there would be no potential issues, but I half-suspect that the transition to a world where size 0 types exist may be more difficult than people would care to accept.Here's a brief summary of my current conclusions. I'd like to eventually explore this again once Regular Void either goes into the language or is killed.1) In order for size 0 types to be useful as a means of minimizing storage, we'd effectively have to change aliasing rules. No two objects of the same type can occupy the same address in C++ as it currently is, which user-level code can and does exploit.
Actually that's not quite true, and it is this exception that focused my stateless subobject idea (that I'm still occasionally refining). There are certain cases where two objects can have the same address. Namely, base class subobjects and their derived class. This is why [basic.lval]/10 has an explicit exception in the aliasing rules for types which are "similar to" the actual object's type.That's why I say two /different/ objects of the /same/ type. EBO doesn't cover that.
On Thu, Jul 7, 2016 at 7:21 PM, Nicol Bolas <jmck...@gmail.com> wrote:I would say that the requirement should be stronger than that. Your idea is to make it a hint, that implementations may allow the size to be 0. I say that it should be enforced. If you declare that a type is zero-sized, then put things in it that aren't themselves zero-sized, then I would say that your code is incoherent and does not deserve to compile. It seems likely to me that a user who declares that a type will be zero-sized, then does something that makes this impossible has made a mistake.It must be a hint to be useful in practice and it is not indicative of a mistake for the use-cases where it is desired. Basically, the end goal is that we want implementations to always prefer to make types such as "struct foo {};" or "struct foo { /*all size 0 datamembers*/ };" as size 0. The problem is that this wouldn't immediately happen in practice because it would be an ABI break. In order for users to actually be able to take advantage of this immediately during the transition, they'd need an additional way to declare types that didn't exist before (hence the specifier) -- this specifier doesn't have to even be a part of the standard, technically, but it would be an extension that I'd expect implementations to ultimately provide and that users would have to take advantage of in order to actually get size 0 types in practice.The reason I say that the specifier must only be a hint and not cause an error if a user were to put a non-static datamember in the type that isn't size 0 is because if it caused an error then it wouldn't help for one of the primary use-cases, which is where datamembers have dependent types that may or may not be size 0 (i.e. a tuple template). You want to be able to declare something like a tuple template in such a way that its instantiations are size 0 when all datamembers are size 0, yet still have the same exact template definition when some of the dependent T types may not be size 0. Applying the specifier on the template definition there would do the trick if it really were just a hint, but if the "hint" were actually something that caused an error if the type couldn't actually be size 0, then the specifier wouldn't be usable there.
But at the same time, I do think it is important that users can make a distinction between "This type will be zero-sided" and "If my declarations make this empty, then it should be zero-sized".
On Thu, Jul 7, 2016 at 7:21 PM, Nicol Bolas <jmck...@gmail.com> wrote:As I've refined my own stateless types idea, I came to realize something. While it is useful to be able to declare that a type will be stateless, it is also useful to declare that a type which was not explicitly declared stateless can be used as a stateless subobject. That way, you don't create a rigid separation between the world of stateless things and the world of non-stateless things.
If someone passes you an allocator that is an empty type, but they didn't declare it stateless, that's OK: you can fix that at the point of use.I'm not sure I follow what you mean. I don't think we are drawing a distinction between stateful and stateless things here. The idea is that an instance of a current C++ type and a size 0 type can be worked with via a common set of abstractions. If whatever we end up does not have such a set of abstractions, then we've failed.
struct empty{};
One of the ways the SFINAE-rule is actually used:
void foo(char (*)[...evaluates to 0 or >0...] =0)
void foo(...) // other overloads
which is (currently) equivalent to (e.g.)
std::disable_if<...evaluates to false/true...>::type foo();
// other overloads
but would not, if zero-sized arrays are allowed (developers would have to replace "[value]" with "[2*(value)-1]" to get a negative-sized array for "false" to still fail SFINAE).
Sean Parent in his key-note talk at cppnow proposed making void a
regular, empty type. At the same time he proposed that when inheriting
from void, you would have a truly empty type. I like that idea.
struct empty{}; // Not really empty.
struct truly_empty: void {}; //This one is!
static assert(sizeof(empty) == 1);
static assert(sizeof(truly_empty) == 0);
/Peter
template<typename Alloc>
struct Me
{
Alloc mine;
};template<typename Alloc>
struct Me : public Alloc
{};Avoiding these issues with syntax and semantics of declaring types that are zero sized was one of my original motivations for trying the idea of using a zero sized array of any type to get a zero sized variable of an empty type.
int i[0];
int *j = i;
*j = 54;Type t[0];
Type *j = t;
j->SomeMemberFunction();As zero sized arrays are not allowed currently there is an open space for defining the exact semantics. As for Matt's original complaints these are valid for other ways to declare zero sized objects but not for zero sized arrays as they have never been allowed before. For instance it is obvious what a for loop over a zero sized array is to do: nothing.
auto start = std::addressof(i);
auto end = ++start;
for(; start < end; ++start)
{...}
It is also easy to define what the size of an array of zero sized arrays is to mean: still an array of a total of zero elements, which follows the logic for all other sizes: multiply all dimension's sizes to get the total size. Furthermore the byte size of zero is consistent with the current way to calculate the byte size of an array variable: multply the align-adjusted element size with the element count of the array.
T t[20];
int i = 0;
for(T &tref : t)
{
++i;
}using T = int[0];As zero sized arrays are not allowed currently there is an open space for defining the exact semantics. As for Matt's original complaints these are valid for other ways to declare zero sized objects but not for zero sized arrays as they have never been allowed before. For instance it is obvious what a for loop over a zero sized array is to do: nothing. It is also easy to define what the size of an array of zero sized arrays is to mean: still an array of a total of zero elements, which follows the logic for all other sizes: multiply all dimension's sizes to get the total size. Furthermore the byte size of zero is consistent with the current way to calculate the byte size of an array variable: multply the align-adjusted element size with the element count of the array.
On Saturday, July 9, 2016 at 4:06:43 AM UTC-4, Bengt Gustafsson wrote:Avoiding these issues with syntax and semantics of declaring types that are zero sized was one of my original motivations for trying the idea of using a zero sized array of any type to get a zero sized variable of an empty type.
Syntax is irrelevant; semantics is the main problem. At present, the semantics of your proposal are quite unclear. You say that zero-sized arrays are a thing, but you don't tell us how they really behave. Or rather, your explanation of their behavior is contradictory, broken, or just confused.
You still haven't answered one of the most important questions. Namely, what does this do:int i[0];
int *j = i;
*j = 54;
Is that legal? If that's not legal, then how could this be legal:Type t[0];
Type *j = t;
j->SomeMemberFunction();
And if that isn't legal, how could you actually do anything with a zero-sized array? How do you access non-static members of such objects? How do you call member functions? And so forth.
On Sat, Jul 9, 2016 at 9:47 AM, Nicol Bolas <jmck...@gmail.com> wrote:On Saturday, July 9, 2016 at 4:06:43 AM UTC-4, Bengt Gustafsson wrote:Avoiding these issues with syntax and semantics of declaring types that are zero sized was one of my original motivations for trying the idea of using a zero sized array of any type to get a zero sized variable of an empty type.
Syntax is irrelevant; semantics is the main problem. At present, the semantics of your proposal are quite unclear. You say that zero-sized arrays are a thing, but you don't tell us how they really behave. Or rather, your explanation of their behavior is contradictory, broken, or just confused.
You still haven't answered one of the most important questions. Namely, what does this do:int i[0];
int *j = i;
*j = 54;
Is that legal? If that's not legal, then how could this be legal:Type t[0];
Type *j = t;
j->SomeMemberFunction();
And if that isn't legal, how could you actually do anything with a zero-sized array? How do you access non-static members of such objects? How do you call member functions? And so forth.I agree with the rest of your post, but I think you're missing the point of size 0 arrays in your examples above. None of those should be legal.
On Monday, July 11, 2016 at 1:58:20 PM UTC-4, Matt Calabrese wrote:On Sat, Jul 9, 2016 at 9:47 AM, Nicol Bolas <jmck...@gmail.com> wrote:On Saturday, July 9, 2016 at 4:06:43 AM UTC-4, Bengt Gustafsson wrote:Avoiding these issues with syntax and semantics of declaring types that are zero sized was one of my original motivations for trying the idea of using a zero sized array of any type to get a zero sized variable of an empty type.
Syntax is irrelevant; semantics is the main problem. At present, the semantics of your proposal are quite unclear. You say that zero-sized arrays are a thing, but you don't tell us how they really behave. Or rather, your explanation of their behavior is contradictory, broken, or just confused.
You still haven't answered one of the most important questions. Namely, what does this do:int i[0];
int *j = i;
*j = 54;
Is that legal? If that's not legal, then how could this be legal:Type t[0];
Type *j = t;
j->SomeMemberFunction();
And if that isn't legal, how could you actually do anything with a zero-sized array? How do you access non-static members of such objects? How do you call member functions? And so forth.I agree with the rest of your post, but I think you're missing the point of size 0 arrays in your examples above. None of those should be legal.
Right, so... what's the point of having a zero-sized array? Remember the stated goal: to allow you to have objects which takes up no space. If `a[0]` for a zero sized array `a` is always illegal, how could you access the object?
You can access the array object perfectly fine, just not an element of the array object, because it doesn't actually have any elements. Examples of things you can logically do with an array of 0 elements are that you can pass the object itself around by reference, you can create iterators into the array (they would be end iterators), you can have a template definition where the array size is dependent and the definition iterates over the array (the code wouldn't fail to compile for the size 0 case and would behave as would be consistent for all other array sizes). Also, depending on how it's specified and if aliasing rules are relaxed for size 0 types, a type that contains a size 0 array (most-likely an instantiation of a template such as std::array) would not contribute to the overall size of the encapsulating object in a way that is somewhat analogous to EBO.Maybe this isn't what some people want it to be, but I'd argue that this is what it logically should be (though aliasing aspects are still a bit subjective).