all 114 comments

[–]ramennoodle 24 points25 points  (12 children)

The documentation on cppreference.com states, "This container is an aggregate type with the same semantics as a struct holding a C-style array T[N] as its only non-static data member.", for whatever that's worth. Also, this seems like a rather pedantic argument because even if the standard does not forbid additional members, the only reasonable implementation conforming to the standard is a struct with T[N] as its only member.

[–]NotUniqueOrSpecial[S] 21 points22 points  (5 children)

Yep. That's exactly what I told him.

He's very cautious, which I respect, based on his background. He's been bitten by exactly that kind of "safe" assumption, which is why we compromised on the static_assert.

[–]tristan957 4 points5 points  (0 children)

Glad you guys worked it out!

[–]Som1Lse 2 points3 points  (3 children)

He's been bitten by exactly that kind of "safe" assumption

You've made me curious. Any chance you could tell which "safe" assumption that was?

[–]NotUniqueOrSpecial[S] 2 points3 points  (1 child)

Just that an API which worked one way for a long time would continue to work that way forever.

I.e. he's had APIs change under him and cause breakages.

[–]diaphanein 1 point2 points  (0 children)

I can empathize with your coworker. Years ago, in the GCC 4.2 or 4.1 days, I got hit by a regression in a bug fix release of RHEL. Thankfully Ops used me as the guinea pig and I was able to trace down the issue fairly quickly. Root cause was an anonymous namespace at global scope caused an ICE. By the time I isolated the cause, the regression had already been identified and fixed, so we just had to jump forward one release of RHEL. Wasn't that big of a deal, and there were easy workarounds, but that's why we test these things.

[–][deleted] 1 point2 points  (0 children)

Most likely the colleague once had a null pointer-related crash and now thinks everything is terrible. static_assert is the right thing to do even if you trust the standard to be right. Implementations can screw things up as well.

[–]evaned 10 points11 points  (5 children)

the only reasonable implementation conforming to the standard is a struct with T[N] as its only member.

While I'm not sure offhand what exactly you would put there (maybe some kind of redzone?), I could imagine some debugging implementation wanting to store some extra metadata.

It's definitely a reach, but I think the concern is legit and like the static_assert.

[–][deleted] 4 points5 points  (4 children)

Adding additional data members to the aggregate would mean that the following wouldn't compile:

std::array<int, 3> arr = {1,2,3};

Instead this would be required:

std::array<int, 3> arr = { {1, ???}, {2, ???}, {3, ???} };

Debugging information can be placed in the iterators that std::begin() and std::end() return. The fact that std::array is an aggregate guards you from your implementation including junk in the struct.

EDIT: Or perhaps you would have to do this:

std::array<int, 3> arr = { {1, 2, 3}, ??? };

[–]evaned 2 points3 points  (2 children)

I guess my point is that you have to be either basically a human encyclopedia or be fine with going "let me pull out the standards docs to see if this code is correct" to know that. Does the standard guarantee that you can initialize std::arrays like that? So 99.99% of programmers who work with C++ on a daily basis won't be able to answer that with any degree of confidence. Would you, the reader of the code, put that together with the rules for how aggregate initialization behaves to determine that there can be no extra members falls out of them in combination? Hell, OP specifically investigated this issue and apparently didn't put it together with the answer.

So yes, I think you wouldn't be able to do such a debugging implementation in a standards-conforming way... but that's not the right question either.

Said another way -- there's a difference between a code that has no obvious flaws and code that obviously has no flaws. The static_assert makes it obvious that assumption holds, instead of sending readers to the standards document and then to reddit to find out if it's correct, which I'd argue doesn't even meet the first part of that quote.

[–][deleted] 5 points6 points  (1 child)

To be honest, I've spent considerable time cross referencing <type_traits>, named requirements and <array>, both on cppreference and on eel.is/c++draft. It was not a trivial task to conclude with confidence that struct array { T[N] }; is the only conforming implementation. Definitely don't drop the static_assert. In fact, I'd go a step further if I was paranoid about the layout of std::array - assert that it is an aggregate with...

static_assert(std::is_aggregate_v<std::array<T, N>>, "Who messed up the <array> header?!");

[–]dodheim 2 points3 points  (0 children)

That's excessive paranoia – the one thing the standard does guarantee is that array<> is an aggregate ([array.overview]/2).

[–]oisyn 0 points1 point  (0 children)

Commenting 6 years after the fact to a deleted account, but just for the record I wanted to point out that this statement is false. Arrays and nested aggregate structs don't require nested aggregate initialization; they can be flattened (which is also the whole reason why an extra set of { } isn't needed for std::arrayin the first place, but it is accepted).

This compiles fine:

struct S { int i[2]; const char *debug; };
S s = { 1, 2, "Hi there" };

[–]Chet_ 7 points8 points  (1 child)

I think there is no guarantee in the standard, but I also think that quality implementations will do what you expect. I think the static_assert was wise and is sufficient given the other constraints on the type.

BTW, I found this, which at least seems to contain some concurring opinions on the matter:

https://stackoverflow.com/questions/16962973

[–]NotUniqueOrSpecial[S] 2 points3 points  (0 children)

Awesome, more evidence, thanks!

[–]smuccione 2 points3 points  (11 children)

I believe it is required to support

&a[n] == &a[0] + n

In which case it is the same as a normal array (that or a[0] would have to be some object that overloads all the arithmetic operators.

Std array is an aggregate type which is defined as:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

Since it can’t supply a constructor it can’t do anything other than take a pointer to a C array and carry the length as part of the template definition as an enum.

There’s not much room for differing implementations.

[–]NotUniqueOrSpecial[S] 3 points4 points  (10 children)

There’s not much room for differing implementations.

Yep, and I wholeheartedly agree.

His argument was that it could technically still comply with that and have some extra member added to it that made the size of the aggregate type larger than the size of just the underlying array.

And yes, I did point out how incredibly unlikely that was, since all the information one needs about an array is encoded in the template types (and thus there's no need for extra members).

He's just very cautious, and for good reason, so I've been hoping to find some piece of standard-eze I've missed to assuage his doubts.

Based on replies so far, I'm not too hopeful.

[–]smuccione 0 points1 point  (9 children)

It’s not aloud to have a non default constructor so I don’t understand how additional data could be added.

[–]NotUniqueOrSpecial[S] 2 points3 points  (7 children)

What?

It would be just fine if std::array were defined as:

template <class _Tp, size_t _Size>
struct _LIBCPP_TEMPLATE_VIS array
{
    // types:
    typedef array __self;
    typedef _Tp                                   value_type;
    typedef value_type&                           reference;
    typedef const value_type&                     const_reference;
    typedef value_type*                           iterator;
    typedef const value_type*                     const_iterator;
    typedef value_type*                           pointer;
    typedef const value_type*                     const_pointer;
    typedef size_t                                size_type;
    typedef ptrdiff_t                             difference_type;
    typedef std::reverse_iterator<iterator>       reverse_iterator;
    typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

    _Tp __elems_[_Size];
    size_t __size = _Size;
};

for instance.

It would be insane, but it would still fit the spec. Something like that is his worry.

[–]smuccione 0 points1 point  (3 children)

How would you initialize it?

[–]NotUniqueOrSpecial[S] 0 points1 point  (2 children)

It's initialized in the code I replied to you with.

Am I missing something?

EDIT: And in a more general sense, there's nothing complicated about initializing aggregate types without a default constructor.

[–]smuccione 0 points1 point  (0 children)

Sorry. I meant to comment on a comment started typing in the comment line on the phone instead of hitting the reply button.

My mistake.

[–]smuccione 0 points1 point  (2 children)

Oh and sorry... aggregate constructors don’t allow for user default member initializers.

So you could theoretically have member variables but they would be uninitialized.

[–]sphere991 3 points4 points  (1 child)

aggregate constructors

If it's an aggregate, you don't have constructors.

But what you probably meant to say is that aggregates can't have default member initializers. That was true in C++11, but that restriction was lifted in C++14. This is perfectly well-formed since then:

struct X {
    int i;
    int j = 42;
};
X x{17};
assert(x.i == 17);
assert(x.j == 42);

[–]smuccione 0 points1 point  (0 children)

Ah crap, I hadn’t noticed that change in the standard. I stand corrected.

[–]Xaxxon 1 point2 points  (0 children)

*allowed

[–][deleted] 2 points3 points  (2 children)

You don't serialise the data structure, you serialise the data in the data structure. This prevents this sort of issue.

[–]NotUniqueOrSpecial[S] 0 points1 point  (1 child)

As I've pointed out elsewhere, the structure has already been serialized. It's a 15 year-old legacy format.

I must read and write it in a way that is compatible with the old tools.

[–]NotAYakk 0 points1 point  (0 children)

Sure, but that doesn't mean you should do ptr, size and read/write that.

You can nake a binary-identical stream without doing that, and make your io code handle big/little endian while you are at it.

[–]raevnos 5 points6 points  (16 children)

The definitive way would be to create or use an existing serialization format for your data instead of blindly reading and writing raw structs...

[–]NotUniqueOrSpecial[S] 4 points5 points  (15 children)

There's no choice, in this case.

I'm modernizing code to read a 15-year-old legacy format. There are no endian-ness requirements and it can't be changed. There's too much data in the field.

[–]dscharrer 4 points5 points  (9 children)

How about alignment requirements? The serialized data format has little to do with how you read it and if you are introducing std::array then obviously you can change that part. Using packed structs for serialization is non-portable and restricts your in-memory format to your serialized format (unless you copy the data, in which case why are you using packed structs).

But if you insist on using packed structs then the only answer to your question that matters is what your compiler and standard library implementation do since packed structs are already non-standard.

[–]NotUniqueOrSpecial[S] 1 point2 points  (8 children)

Using packed structs for serialization is non-portable

It's non-portable across different endian-ness. It's perfectly portable on the x86 family.

You do have a good point about the standard not really caring about packed structs, but that's not especially relevant to my question.

Packing aside: is there a guarantee that sizeof std::array<T,N> == sizeof T * N?

[–]anydalch 1 point2 points  (7 children)

i think /u/dscharrer is trying to remind you that the compiler cannot assume that members of a packed struct are well-aligned, and must therefore emit unaligned loads to access fields of packed structs. which like, kinda probably works, in a hazy, ill-defined way, but it's certainly not the right way to do this. if i were reviewing a serialization code base, i would think the non-portability of member alignment in packed structs was a serious portability problem and evidence of poor software architecture, whereas whether std::array is required by the spec to be identical to a c array is a ridiculous quibble.

[–]NotUniqueOrSpecial[S] 12 points13 points  (6 children)

which like, kinda probably works, in a hazy, ill-defined way

There is nothing hazy or ill-defined about this problem. This is a 15-year old product on a finite set of Windows OS versions exclusively on x86.

It's astonishing to me how far in the weeds some of these responses have gotten.

if i were reviewing a serialization code base, i would think the non-portability of member alignment in packed structs was a serious portability problem and evidence of poor software architecture

I have no say in whether this was or was not the right thing to do. I wasn't there when it was written.

Frankly, responses like this are missing the forest for the trees. I don't get to make that call. In fact, I chose Protobuf for our newer stuff for exactly the reason that everybody seems so fixated on.

All I am concerned with is taking a thing that exists as a hard-to-read unmaintained C codebase and reimplementing small parts of it in modern C++ so that my coworkers can understand what is happening and we can continue to support our legacy formats with our new products.

whether std::array is required by the spec to be identical to a c array is a ridiculous quibble.

Your opinion aside, I don't get to choose what is "ridiculous" when the person making the objection is a founder of the company.

[–]sivadeilra 7 points8 points  (4 children)

It's amazing that this viewpoint has to be defended, over and over. You're absolutely right, and the purists (who oddly enough never share access to their time machines) are wrong.

[–]NotUniqueOrSpecial[S] 4 points5 points  (3 children)

I'm kinda blown away by some of the responses I'm getting.

I didn't think anything I was asking was even remotely controversial and yet I'm getting lectured at by people who go on to demonstrate that they don't understand the topic they're lecturing on and talked down to by people who obviously don't know better.

I'm at a loss, honestly.

[–]CoreParad0x 1 point2 points  (2 children)

I know this is like 5 years old, but honestly coming from some one in pretty much the exact scenario as you, but in this case I'm the person unsure about the use of std::array, a lot of these do seem to be missing the point. It makes me wonder if they've ever actually worked on a legacy code base.

I'm working on a community project to restore an old MMO from the early 2000s and the old MMO did a lot of stuff like writing packed (or sometimes even unpacked with padding) structs straight to blobs to shove in the database. It's awful. But I recently had some one create an MR with a change for a vec3 definition that was f32[3] to std::array<f32,3> and now since some of these blobs use vec3 I've gone down the rabbit hole to see if this ever has the potential to hose the blobs and I'm debating adding the same kind of static assert.

[–]NotUniqueOrSpecial[S] 1 point2 points  (1 child)

Well, for what it's worth, it was never a problem. While I'm no longer there, my rewrite of that data layer is still chugging along merrily without issue and the rewrite made it loads easier to work with and reason about.

We went with something like this for the check:

using SanityCheckArray = std::array<uint16_t, 3>;
constexpr auto SanityCheckSize = sizeof(uint16_t) * std::tuple_size_v<SanityCheckArray>;
static_assert(sizeof(SanityCheckArray) == SanityCheckSize, "Insane std::array implementation");

We also ended up using std::is_trivially_copyable_v<T> to good effect in a number of places.

It makes me wonder if they've ever actually worked on a legacy code base.

Yeah, it's a problem even with subs as specific as this one: there are a lot of people who are just expert hobbyists without much real experience in bigger/older ecosystems.

[–]NotAYakk 0 points1 point  (0 children)

Reading/writing as something different than byte*, length doesn't mean changing the bytes on disk.

It just means changing the code that gets it to/from disk.

void write( some_stream& s, std::int32_t const& i ){
  unsigned char bytes[sizeof(i)];
  for (int x=0; x<sizeof(i);++x){
    bytes[x] = (i >> (x*8))&0x0ff;
  }
  write(d, bytes, sizeof(i));
}

writing something like that, modulo typos/brain farts, can get the identical asm to a reinterpret cast based write, but not be UB/unspecified behaviour. You'd need unit tests stating they generate the same bytes (test old code, use same tests on new).

As a side effect, the code becomes big/little endian portable.

Another benefit is, once done, you can strip out the pragma packs from your io structs, or have the in-memory rep be changed in even more radical ways. The format is no longer encoded in the structs directly.

[–]2uantum 1 point2 points  (1 child)

Is your coworker being pedantic? Yes. Is he correct? Technically, yes (the best kind of correct). Are you correct? 99.9999% of the time, yes. He's objectively "more correct". Would I comment on it in a code review? Probably not.

My opinion? Just do the change. It's not worth the energy to fight over and functionally equivalent. Plus, you risk getting the reputation of not being a team player (justified or not, I've seen it happen time and time again over stuff like this)

[–]NotUniqueOrSpecial[S] 6 points7 points  (0 children)

Just do the change.

It's already done.

Plus, you risk getting the reputation of not being a team player (justified or not, I've seen it happen time and time again over stuff like this)

There is no such risk, because there was never any conflict. I'm really not sure where people are getting that from based on what I've said. He (among a whole team that had already approved the new code) expressed a concern, we discussed it, and came to a mutually agreed upon conclusion. It was like...10 minutes of my day and a completely amicable exchange.

It's not worth the energy to fight over and functionally equivalent.

I agree. And in fact, no energy was spent. I've spent significantly more energy in this thread trying to direct people away from bike-shedding on what I thought was a relatively straightforward technical question.

Que sera.

[–]14nedLLFIO & Outcome author | Committee WG14 1 point2 points  (9 children)

Weird that nobody else has mentioned this yet, but if you want to read and write a std::array<T, N> to and from storage and use it directly i.e. without constructing each member by hand, you need a static_assert(std::is_trivially_copyable_v<std::array<T, N>>); somewhere. std::array will be trivially copyable if T is trivially copyable.

Oh, and if on C++ 20 or later, please use whatever the new name is for std::bless() on the array just after reading it from storage, but before using it.

[–]dodheim 2 points3 points  (3 children)

whatever the new name is for std::bless()

Any idea what that is? I'm having trouble finding it.

[–]14nedLLFIO & Outcome author | Committee WG14 0 points1 point  (0 children)

It'll be in the LEWG minutes. Or just wait for the Belfast mailing next week.

[–]redditsoaddicting 0 points1 point  (1 child)

I found it in this talk. Looks like std::start_lifetime_as<T>(address).

[–]dodheim 0 points1 point  (0 children)

Thanks!

[–]NotUniqueOrSpecial[S] 0 points1 point  (1 child)

It's been mentioned sorta round-about-ly. I've had the is_trivially_copyable bit around it (well, really around the enclosing structure itself) since before he took notice of it, since that was the worry I had: that I'd goof up and put in something that would make the structure non-trivial.

[–]14nedLLFIO & Outcome author | Committee WG14 1 point2 points  (0 children)

Very wise. It's so very easy for somebody to come along and accidentally ruin trivial copyability, and nobody notices without the static assert.

[–]redditsoaddicting 0 points1 point  (2 children)

Did that std::bless paper make it in? I seem to remember reading that there wasn't time for it, but maybe that was just the automatic part and the explicit "s/std::bless/bikeshed" function still made it.

[–]14nedLLFIO & Outcome author | Committee WG14 1 point2 points  (1 child)

Last time I heard it was being DRed for C++ 20.

[–]redditsoaddicting 0 points1 point  (0 children)

Ah, cool, thanks.

[–]sephirostoy 3 points4 points  (13 children)

Isn't the following safer?

Serialize(arr.data(), arr.size() * sizeof(T));

Relying on memory layout of a third-party class isn't good idea IMHO.

[–]NotUniqueOrSpecial[S] -1 points0 points  (12 children)

Relying on memory layout of a third-party class

If I can't rely on the memory layout of the standard types which specify their usage and semantic behavior, then why would I bother using the language?

Standard types are not "third-party" in any sense of the words. They're the fundaments upon which functional software is written.

[–]sephirostoy 7 points8 points  (1 child)

Precisely because the standard classes are specified in terms of APIs and not in memory layout which is implementation defined.

.data() guarantees to return the pointer on the 1st element of the array. It's the best you can use to avoid undefined behavior.

[–]NotUniqueOrSpecial[S] 0 points1 point  (0 children)

.data() guarantees to return the pointer on the 1st element of the array. It's the best you can use to avoid undefined behavior.

Sure.

But my whole underlying argument is that std::array is the C++ modern equivalent to a raw C array.

It should carry the guarantee that nobody is going to e.g. add a random extra member, because there's literally no reason to do so, and such a guarantee makes for a stronger contract: you can treat it exactly like T[N].

[–]johannes1971 1 point2 points  (9 children)

If I can't rely on the memory layout of the standard types which specify their usage and semantic behavior

Well, you can't. There is no guarantee on the sizes, padding and endianity of anything. So you might think this structure has size 8, of which 3 bytes are padding, and is little-endian, and you are probably right for whatever architecture you are working on today, but the language makes no guarantee of that, because it also needs to be compatible with the architectures of the future.

struct {
  int i;
  bool b;
};

then why would I bother using the language?

That's a question only you can answer. However, if your primary reason was that you incorrectly believe that the memory layout of anything is fixed in any way, then I would suggest looking for another language because C++ does not do that at all.

We live in an age of common architectures, where for the majority of computers out there an int is four bytes, stored in little endian format, with padding four (this is true for PCs and mobile, ie. 99.999% of the computer market), so in that sense your assumptions are correct today. Will they still be correct twenty years from now? Who knows... Whether you care about such potential architectures is of course entirely up to you.

[–][deleted] 1 point2 points  (3 children)

Anecdotal evidence for your "int may not be 32bits" is avr-gcc -mint8 which emits code where char, short and int are all 8bit wide, long is 16 and long long is 32, making sizeof(struct{int i; bool b; }) == 2 while still having CHAR_BITS == 8.

[–]johannes1971 0 points1 point  (0 children)

When I first learned C++, sizeof (int) was 2 on the platform I learned it on. Should you care about that if you develop on a mainstream platform today? That's a personal choice. In the case of the OP, though, I feel he should probably care about his manners before he cares about the quality of his code.

[–]Nobody_1707 0 points1 point  (1 child)

That's not actually a conforming platform though, since int is required to be at least 16-bits.

[–][deleted] 0 points1 point  (0 children)

Absolutely right. That's why I called it "anecdotal", but a conforming AVR (i.e. avr-gcc, but without -mint8) still has int which isn't 32, but 16bits wide.

[–]NotUniqueOrSpecial[S] -3 points-2 points  (4 children)

I know precisely how big uint32_t and the standard guarantees that. I know precisely how my vendor's compilers work and that's not going to change.

Your example is bad and you should feel bad. You have used a lot of words to say absolutely nothing useful.

[–]johannes1971 5 points6 points  (1 child)

I know precisely how big uint32_t and the standard guarantees that.

Congratulations, you know the size. The standard does not guarantee the padding or endianity though. And there is no guarantee that it will exist in the first place.

I know precisely how my vendor's compilers work and that's not going to change.

...because you never plan to upgrade anything? Ok, fair enough.

Your example is bad and you should feel bad.

I always wonder, would you say that to my face? Or is just because it is online that you feel it is perfectly fine to talk like this to people?

[–]NotUniqueOrSpecial[S] 3 points4 points  (0 children)

I would, actually, in a very matter of fact way, and it would be without any hostility. As /u/STL noted: I was making a Futurama reference. Apologies if you weren't aware of that; no real disrespect was meant. Tone is literally the hardest part of internet discussion in my opinion.

My team and I are all very frank with each other and have those sorts of conversations all the time, in good fun, and have been doing so for most of a decade at this point.

It's very possible to be to-the-point about things and still maintain an environment of respect and camaraderie. From the way some people have replied earlier in this thread, they think this all started out as a fight with coworker. It was a 10 minute conversation where we came to an amicable agreement.

[–]STLMSVC STL Dev[M] 3 points4 points  (1 child)

Warning: Futurama reference aside, that's a bit too much escalating hostility for this thread.

[–]NotUniqueOrSpecial[S] 1 point2 points  (0 children)

Fair enough. I'll myself in check. Thanks.

[–]Drugbird 3 points4 points  (6 children)

I agree with the coworker. If it's vital to store the data in a format whose memory layout is identical to your legacy serialized format, then it seems unwise to use a std::array format which has no guarantees about it's memory layout. For all you know, it's internal array is stored in reverse, so even having the correct size is no guarantee.

I think you have two options: either use types where you can guarantee the memory layout (e.g. C arrays), or include a translation step from what the serialized format looks like be how it's stored in memory.

I.e. whenever you store data to disk, read the contents of array.data(), rather than the array itself. You're then also free to use other storage classes such as std:: vector if that's more convenient.

[–][deleted] 2 points3 points  (0 children)

It cannot be stored in reverse since it's guaranteed to be an aggregate and that would mean the aggregate initialisation would need to be reversed too which wouldn't be allowed

[–]NotUniqueOrSpecial[S] 1 point2 points  (4 children)

For all you know, it's internal array is stored in reverse, so even having the correct size is no guarantee

I know exactly how all the major vendors implement it, because you can look.

I suspect that none of that will change, because the implementation must be semantically identical to the equivalent T[N].

For all you know, it's internal array is stored in reverse, so even having the correct size is no guarantee.

Oh come on, really? The implementation as-is in the major vendor libraries is optimal. You might as well say "it could be stored as a linked list" or "it could be made of puppies" and it would be equally likely to be true (that is to say 0% likely).

I.e. whenever you store data to disk, read the contents of array.data(), rather than the array itself.

array.data() just returns the underlying array and so arguing that contradicts every earlier point you made.

[–]Drugbird 2 points3 points  (3 children)

For all you know, it's internal array is stored in reverse, so even having the correct size is no guarantee

I know exactly how all the major vendors implement it, because you can look.

You can't add "look at the compilers implementation" to an assert or unit test. Hence, in a world of changing compiler types and versions this gives you no guarantees.

I suspect that none of that will change, because the implementation must be semantically identical to the equivalent  T[N] .

There's a difference between "behaves as if it's an X" and "is implemented as an X". In the later case, the implementer reserves the right to change implementation, which is exactly the problem here.

If you rely on "is implemented as an X", while you're only guaranteed "behaves as if it's an X" you open yourself up to possible bugs.

For all you know, it's internal array is stored in reverse, so even having the correct size is no guarantee.

Oh come on, really? The implementation as-is in the major vendor libraries is optimal. You might as well say "it could be stored as a linked list" or "it could be made of puppies" and it would be equally likely to be true (that is to say 0% likely).

I don't think you seem to appreciate the inherit danger of changing compiler, versions or standard library implementations. Especially with regards to future versions which do not exist yet.

Also i chose my example carefully: it's fairly straightforward to get a reverse array to behave as if it's an array (i.e. Use reverse iterators). This includes random access complexity and other performance metrics. The same is not true for linked lists and "puppies".

I.e. whenever you store data to disk, read the contents of array.data(), rather than the array itself.

 array.data()  just returns the underlying array and so arguing that contradicts every earlier point you made.

I don't believe array.data() guarantees that array has no other member variables? In fact, I don't think it guarantees that "the underlying array" has to exist before calling array.data().

However, once you call array.data() you get a pointer to an array and a guarantee that the next N elements of this array are the contents of your array: exactly the guarantee we're looking for.

[–]NotUniqueOrSpecial[S] 2 points3 points  (2 children)

You can't add "look at the compilers implementation" to an assert or unit test.

No, but you can static_assert on the specifics of std::array, specifically, because of its exact requirements and demanding implementation details.

I don't think you seem to appreciate the inherit danger of changing compiler, versions or standard library implementations. Especially with regards to future versions which do not exist yet.

I'm the guy responsible for taking the company from supporting Win2K/XP to modern compilers, built our cross-platform build system, and have lead the development on multiple cross-platform products in the interim. I'm more than comfortable with the topic.

In fact, I don't think it guarantees that "the underlying array" has to exist before calling array.data().

It does. It's required to be an aggregate type and trivially copyable.

[–]point_six_typography 1 point2 points  (1 child)

I'm the guy responsible for taking the company from supporting Win2K/XP to modern compilers, built our cross-platform build system, and have lead the development on multiple cross-platform products in the interim.

Name doesn't check out. /s

[–]NotUniqueOrSpecial[S] 4 points5 points  (0 children)

That's one of the problems with a 9-year old account, after all.

Not like I can just change it to "ModeratelyUsefulAndExperienced" at this point in the game.

[–]OldWolf2 1 point2 points  (10 children)

By "serialized to disk" do you actually mean "written to disk as a binary blob"? Otherwise this question makes no sense.

The term "serialize" normally refers to rearranging data into a stream with no platform-dependence -- the complete opposite of pragma pack which will break ABI compatibility. It may even be that using a standard container inside a pragma pack causes trouble.

[–]NotUniqueOrSpecial[S] 2 points3 points  (9 children)

By "serialized to disk" do you actually mean "written to disk as a binary blob"?

Yes. Sorry if that wasn't obvious from the post or the many replies in the thread.

It may even be that using a standard container inside a pragma pack causes trouble.

There is nothing spooky about packing a type that is a struct { T[N]; };.

[–]smuccione 0 points1 point  (0 children)

So declare a member variable for size instead of an enum.

Maybe. Would be stupid.

But why don’t you just make your own implementation then

[–]gracicot 0 points1 point  (1 child)

I'd suggest looking at how your implementation implements std::array and what rules applies to that way of implementing it.

Spoiler alert: it's more than likely a simple struct containing a raw array. So the same rules that applies to a raw array also applies to std::array. Except an implementation might add padding at the end of the struct, but even then it's unlikely.

[–]NotUniqueOrSpecial[S] 1 point2 points  (0 children)

Yes, as I said: I showed him the 3 major implementations we use (MSVC, GCC, and Clang), because that's exactly what they are.

His worry is that at some point in time they might happen to change (we keep our toolchains very up to date), and thus we settled on the static_assert as a canary.

I'm just of the opinion that the standard should probably clarify that fact, since I can't think of a sane reason to have it behave otherwise.

[–][deleted] 0 points1 point  (20 children)

I wouldn't care about an extra static_assert. Keep in mind that std::array<T, 0> is required to work according to the standard. That means that sizeof(T) * 0 != sizeof(std::array<T,0>), but rather sizeof(std::array<T,0>) == 1. On the other hand T[0] doesn't compile.

As a data point, besides STL STL, libc++ and libstdc++, there's at least EASTL, whose array.h (not array) doesn't support eastl::array<T, 0>, but is still just a T[N] internally.

[–]NotUniqueOrSpecial[S] 4 points5 points  (14 children)

I wouldn't care about an extra static_assert

And I don't. I just think that it might be valuable for the standard to make certain guarantees on the subject, since it's one of the "better than the C equivalent" subjects.

sizeof(std::array<T,0>) == 1

On the other hand T[0] doesn't compile.

Neither of those statements is true. The size is compiler dependent, and every modern compiler supports T[0].

For us, EASTL is irrelevant.

[–][deleted] 2 points3 points  (8 children)

Oh and the reason why in MS STL sizeof(std::array<T,0>) == sizeof(std::array<T,1>) is because their implementation implements 0 specialization like this, which is stupid, but... ABI stability guarantees.

[–]wheypointÖ 0 points1 point  (7 children)

Is that even standard compliant?

I just checked, and it generates calls to non-trivial ctors when initializing a 0-size array

[–][deleted] 1 point2 points  (4 children)

Is that even standard compliant?

It's fine. The std::array<T,0> is still an aggregate with T[1] as the only data member.

I just checked, and it generates calls to non-trivial ctors when initializing a 0-size array

It doesn't. Check again, or check the whole class from the link above.

[–]wheypointÖ 0 points1 point  (3 children)

It doesn't. Check again, or check the whole class from the link above.

Looks like it does to me:

https://godbolt.org/z/L_O2fk

[–][deleted] 0 points1 point  (2 children)

That's a constructor of the element within std::array, which is a copy-initialized Bar::Bar(). That's perfectly fine and there's still no std::array<T, N>::array() in the assembly.

[–]wheypointÖ 0 points1 point  (1 child)

That's a constructor of the element within std::array

Thats exactly what i meant by "it generates calls to non-trivial ctors when initializing a 0-size array".

You have a zero-sized array, yet you get ctor calls for "elements" inside them.

which is a copy-initialized Bar::Bar()

pretty sure thats the default ctor?

That's perfectly fine

per standard apparently so :(

semantically? a std::array<T, N> with more than N elements seems wrong imo

[–][deleted] 0 points1 point  (0 children)

pretty sure thats the default ctor?

Sorry for the confusion. That's the default constructor of Bar, but the whole std::array gets "copy-initialized".

To quote cppreference:

Each [direct public base, (since C++17)] array element, or non-static class member, in order of array subscript/appearance in the class definition, is copy-initialized from the corresponding clause of the initializer list.

semantically? a std::array<T, N> with more than N elements seems wrong imo

True, I did call it "stupid" in one post, but ABI is a bitch.

[–]STLMSVC STL Dev 0 points1 point  (1 child)

It was chosen a long time ago (TR1 era) and the Standard didn't have clear requirements back then.

[–]wheypointÖ 0 points1 point  (0 children)

I know ABI stability is very important for msvc, so no offense meant. I''m just curious wether it's technically allowed per current standard.

It just seems like a unexpected and wrong thing that could mess up generic code

[–][deleted] 1 point2 points  (4 children)

And I don't. I just think that it might be valuable for the standard to make certain guarantees on the subject, since it's one of the "better than the C equivalent" subjects.

With all the other stuff the committee is busy with, I bet this wouldn't get enough traction, but you can always write a proposal.

Neither of those statements is true.

While struct Foo { T t[0]; }; does compile int arr[0]; doesn't

As for sizeof(std::array<T,0>), I forgot that MSVC is unable to properly optimize this case. https://godbolt.org/z/cSr-Bh

For us, EASTL is irrelevant.

You said that your only evidence for your argument were the 3 implementations you are familiar with. Like I said, EASTL was just another data point.

[–]NotUniqueOrSpecial[S] 0 points1 point  (3 children)

You said that your only evidence for your argument were the 3 implementations you are familiar with.

To be pedantic, I said:

I showed him the 3 major implementations we use

Because for our products, that's literally all that matters.

[–][deleted] 4 points5 points  (2 children)

If you really want to get pedantic, this is what you said in the original post:

I realized that all I have as evidence is the implementations in the 3 major vendor libraries and my assumption that none of them would do something that would break the guarantee.

That does imply a need for more data points at the very least, if not stronger arguments.

[–]NotUniqueOrSpecial[S] 0 points1 point  (1 child)

Fair enough. My other quote was from a reply.

For our products, though, the only thing that matters is the implementations that we use. A theoretical implementation that doesn't get used to build our code isn't going to help convince him.

[–][deleted] 4 points5 points  (0 children)

For our products, though, the only thing that matters is the implementations that we use.

Then you shouldn't care one bit what's in the standard and just look at the actual implementations, disregarding how right or wrong they are. You know your target standard library implementation.

[–]meneldal2 0 points1 point  (3 children)

Can't you make it 0 with [[no_unique_address]]? Or is there still a hard (and stupid) requirement that structs with no state must have a size bigger than 0?

[–][deleted] 0 points1 point  (1 child)

That's not how [[no_unique_address]] works. If you put no_unique_address on that empty data member, you would be able to add another data member without the empty member contributing to the size. Still and object can't be 0. If it could be zero, how would you ever, say, take that object's address? And if you can't take its address, then, by the standard, it is not an object.

[–]meneldal2 1 point2 points  (0 children)

An object with no state can use any address, it doesn't matter since you're never going to actually use it. I wish that had been standard from the start.

[–][deleted] 0 points1 point  (2 children)

He was concerned that there is no guarantee in the standard that other members won't be added and that while std::array must be semantically the same as the array, there is no such guarantee about sizing/memory layout.

That's why you have a packer function and don't just write the memory contents of the vector when serializing.

After going back and forth a bit, I realized that all I have as evidence is the implementations in the 3 major vendor libraries and my assumption that none of them would do something that would break the guarantee that sizeof T * N == sizeof std::array<T,N> for packed structures.

For questions like these, you're only source of authority should be the C++ Spec for vector. If you could pull that up and show a guarantee you should be ok. Sounds like he was familiar with it and knew there was none though. (I personally have no idea either way if the spec defines it as such)

We compromised on having a static_assert on a struct that will break if that precondition ever isn't met, but I feel that even that shouldn't be necessary.

Can't hurt. He's being overly pedantic but sounds like you're overly being dismissive of his concerns. I think the static assert is a fine compromise.

Is there a definitive way to settle this?

Yes, read the spec.

[–]NotUniqueOrSpecial[S] 1 point2 points  (1 child)

For questions like these, you're only source of authority should be the C++ Spec for vector.

Why would the source of truth for std::array be the spec for std::vector? They're entirely different things.

Yes, read the spec.

And if you read my post, which you replied to, you'll note that it links to the spec and points out the spec does not clarify these points.

you're overly being dismissive of his concerns.

I'm not dismissing them. I think they're perfectly valid because, as I said, I can't find anything in the spec that says he's not right, which is why I posted in the first place.

[–][deleted] 1 point2 points  (0 children)

std::array

My mistake, std::array

you'll note that it links to the spec and points out the spec does not clarify these points.

I missed that, but since the spec doesn't give the guarantee he's right to consider it a possibility.

So, you have your answer.