Is it safe to assert(sizeof(A) == sizeof(B)) when A and B are "the same"?
Asked Answered
K

4

19

Suppose I have two classes that I would expect to have exact same memory layout:

struct A {
    int x;
    int y;
};

/* possibly more code */

struct B {
    int a;
    int b;
};

Is there anything in the standard that guarantees that I can safely static_assert(sizeof(A) == sizeof(B)) ?

As a weaker variant consider

struct C {
    int a;
};

static_assert( sizeof(A) >= sizeof(C) );   // can this ever fail?
static_assert( sizeof(A) >  sizeof(C) );   // can this ever fail?

The question was triggered by this one. Naively I would not expect any of the asserts to fail, but is this guaranteed?

Kaolin answered 8/3, 2019 at 18:0 Comment(18)
Have you seen this post before? In your example, on the same compiler, targeting the same platform, with the same level of optimizations I think it's safe to say that two equivalent POD structs will be the same size. The standard avoids making such guarantees though.Roti
@CoryKramer yes, I know about padding and that made me wonder if the compiler is constraint to use same padding for A and B or free to pad them differentlyKaolin
Don't you want static_assert rather than assert so you get a compile time error rather than a run time error, when things go bad (and assert is also, usually, compiled out in release builds. static_assert cannot be ignored).Virilism
In this scenario -- struct A {int x; int y;}; ...some code... struct B {int a;int b;}; -- Not guaranteed to be the same size. It all depends on the implementation details of "some code". Probably that's why the standard may not mention this -- too many edge cases would make the sizes different.Zebapda
When you say safe, do you mean is there an implementation where the assert would trigger?Placentation
@Placentation "safe" in the sense: an implementation where the assert triggers is not standard compliant. Not "safe" in the sense of "no sane implementation would make them fail" ;)Kaolin
@Zebapda actually I was suspecting this, but wasnt sure. "no" is a valid answer, though non-existance is much harder to prove than existance of something ;)Kaolin
Is your question about the assert statements themselves, or about code that is relying on this behavior? Either way, don't hesitate to add static_asserts in your code, even if you are not sure about how the standard handles your specific situation. Code with static_asserts to validate assumptions is always safer than code that makes assumptions without validation. If at some point you find some platforms/situations where these asserts do fail, then congratulations: you just caught a bug at compile time that you otherwise probably would have missed.Bismuthic
@Bismuthic sorry if i wasnt clear on that, the static_asserts are just for demonstration, I am not worried about the static_assert themself, but about any code that might make the same assumption silently. Using static_assert to validate assumptions is a good suggestion and sometimes I even use it to documentKaolin
Why not just use A? Why do you need B? Do they have different methods or something?Available
@Bismuthic actually the question that triggered this one was about compiler optimizations, so in fact the question is really just about what does the standard say and the example is rather academicKaolin
@Chipster I dont need either of them, they are just the most simple example I could come up with to ask this quesiton ;)Kaolin
@Chipster anyhow, consider struct bank_account { int money; int max_deposit; }; and struct rectangle { int height; int width; };, I wouldnt want to use a rectangle in place of a bank_accountKaolin
I got you. It's an academic exercise. Based on the accepted answer to the other question you gave, I don't see why it would have a problem, but I'm way over my head with compiler optimizations.Available
Unfortunately the standard has under specified what layout-compatible means, unless I'm missing something (which has been known to happen). It could be a defect, or maybe they wanted to be unspecified. If these objects were in a union it is safe to access the members of the other object and this seems to back it up that it will have to be the same but I'm not sure.Placentation
Ah. Your second comment makes even more sense. Not sure why you'd assert that bank account is equal in size to a rectangle, but I'm already reading way too far into this. Ignore my over analyzing brain 😄 To be fair, I thought this was real code and was trying to keep you from making two completely identical classes for no good reason, because someone would really ask that I'm sure.Available
@Chipster questions tagged as language-lawyer are about the formal rules of c++ as you can find them in the standard. Of course they do apply also to real code, but it doesnt need real code to discuss themKaolin
"Is it safe?" If it isn't then it is critically important that you do include these assert checks. For example if working with binary data structures in disk files, especially memory mapped files, you want guarantees that your struct and its members have exact sizes and byte offsets whether you compile on 16-bit or 64-bit.Drawbar
R
2

Nothing in the Standard would forbid an implementation which identified all the structures that are ever used as parts of unions, and added a random amount of padding after each element of any structure that was not used in such fashion. On the other hand, nothing would forbid an implementation from behaving in arbitrary fashion if the number of tags an implementation can handle, nor would anything forbid an implementation from imposing a limit of one.

All of those things fall into the category of things that the Standard would allow a conforming implementation to do, but which quality implementations should generally be expected to refrain from doing even if allowed by the Standard. The Standard makes no effort to forbid implementations from doing silly things, nor to guess at whether some specialized implementations might have a good reasons for processing something in an atypical fashion. Instead, it expects that compiler writers will try to fulfill the needs of their customers whether or not the Standard requires them to do so.

Rinker answered 8/3, 2019 at 21:45 Comment(3)
I believe that there is something in the standard preventing this: the term "layout-compatible structs" (see my answer).Coaster
@JimOldfield: If the Standard were to specify that for any program, regardless of whether it exceeds translation limits, a conforming implementation must either process it correctly or else, in some Implementation-Defined manner, indicate a refusal to do so, then many goofy behaviors would be classifiable as non-conforming. From what I understand, however, the ability to process any particular program meaningfully, or indicate in meaningful fashion when programs can't be processed meaningfully, are quality-of-implementation issues which aren't actually required for conformance.Rinker
Sorry but I don't understand your comment. What have translation limits got to do with anything? Or "programs [that] can't be processed meaningfully"?Coaster
U
2

A contrived counter-example:

#include <stdint.h>

struct A {
    int32_t a;
    int64_t b;
};

#pragma pack(4)

struct B {
    int32_t a;
    int64_t b;
};

static_assert(sizeof(A) == sizeof(B));

Compilation with g++ in 64-bit Linux yields:

a.cc:15:1: error: static assertion failed
static_assert(sizeof(A) == sizeof(B));
Unorganized answered 9/3, 2019 at 18:47 Comment(1)
The question asked about "...anything in the standard that guarantees..." but #pragma effects are non-standard, so this doesn't answer the question. (It is a useful side comment though.)Sarpedon
C
2

Yes, the standard guarantees that it is safe to assert that. The relevant term here is layout compatible.

The standard defines that term in two parts. First it defines what a common initial sequence of data members is (but only for standard-layout structs): It is the sequence of data members that are equivalent between the two structs. The standard includes an example, but I'll use a slightly different one to avoid some technicalities:

struct A { int a; char b; };
struct B { int b1; char b2; };
struct C { int x; int y; };

In that example, the common initial layout of A and B is both of their members, while for A and C it is only their first respective members. It then defines structs as layout compatible if the common initial layout is simply the whole class.

If you have instances of two different layout-compatible types, like A and B in the above example, you *can* assume they have the same size:

static_assert(sizeof(A) == sizeof(B));

However, you *cannot* (in theory) cast between them without invoking undefined behaviour, because that violates aliasing rules:

A a{1, 'a'};
B* b = reinterpret_cast<B*>(&a); // undefined behaviour!
do_something_with(b);

What you can do, subject to the usual const/volatile rules as well as rules about data members being trivial (see When is a type in c++11 allowed to be memcpyed?), is use memcpy to get data between layout-compatible structs. Of course, that wouldn't be possible of padding between members could be randomly different, as the current top answer suggests.

A a{1, 'a'};
B b;
memcpy(&b, &a, sizeof(b)); // copy from a to b
do_something_with(b);

If do_something_with takes its argument by reference and modifies it then you would then need to copy back from b to a to reflect the effect there. In practice this would usually be optimised to be what you would expect the above cast to do.

The answer by atomsymbol gives an example that appears to contradict everything above. But you asked about what was in the standard, and a #pragma that affects padding is outside of what is covered by the standard.

Coaster answered 11/1, 2021 at 17:9 Comment(3)
Thanks for the detailed answer. There is just one detail that bugs me: Why does layout compatible imply static_assert(sizeof(A) == sizeof(B)); ? They would be layout compatible in case there is padding after the last member, but then they could have (in theory) have different size, no?Kaolin
Also I am curious if something changed regarding this since C++03, but thats far beyond the scope of the question. I think your gave enough pointers so I can find out myselfKaolin
@largest_prime_is_463035818 If you made wrapper structs struct A2 { A as[2]; } and struct B2 { B bs[2]; } then those are also layout compatible. But that means &as[1] and &bs[1] must have the same offset. There can't be any padding between elements of an array (sizeof(Foo[n]) == n * sizeof(Foo)) so it must mean that sizeof(A) == sizeof(B).Coaster
F
-5

The only instance where you assertion can be false is when there is a difference of packing criteria. Otherwise the assertion must be true.

The compiler only has the struct definition to work out member offset so the so unless the layoùt is consistent you would not be able access the struct.

Faucher answered 8/3, 2019 at 18:42 Comment(1)
I think we all agree that this would be the logical effect, however the question is about this being guaranteed by the standard.Petropavlovsk

© 2022 - 2024 — McMap. All rights reserved.