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?

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 2
    Have you seen [this post](https://stackoverflow.com/questions/119123/why-isnt-sizeof-for-a-struct-equal-to-the-sum-of-sizeof-of-each-member) 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. – Cory Kramer Mar 08 '19 at 18:02
  • @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 differently – 463035818_is_not_an_ai Mar 08 '19 at 18:04
  • 3
    Don't you want [static_assert](https://en.cppreference.com/w/cpp/language/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). – Jesper Juhl Mar 08 '19 at 18:05
  • 2
    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. – PaulMcKenzie Mar 08 '19 at 18:10
  • When you say safe, do you mean is there an implementation where the assert would trigger? – NathanOliver Mar 08 '19 at 18:11
  • 3
    @NathanOliver "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" ;) – 463035818_is_not_an_ai Mar 08 '19 at 18:12
  • @PaulMcKenzie 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 ;) – 463035818_is_not_an_ai Mar 08 '19 at 18:13
  • 1
    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_assert`s in your code, even if you are not sure about how the standard handles your specific situation. Code with `static_assert`s 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. – 0x5453 Mar 08 '19 at 18:26
  • 2
    @0x5453 sorry if i wasnt clear on that, the `static_assert`s 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 document – 463035818_is_not_an_ai Mar 08 '19 at 18:30
  • Why not just use A? Why do you need B? Do they have different methods or something? –  Mar 08 '19 at 18:31
  • @0x5453 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 academic – 463035818_is_not_an_ai Mar 08 '19 at 18:32
  • @Chipster I dont need either of them, they are just the most simple example I could come up with to ask this quesiton ;) – 463035818_is_not_an_ai Mar 08 '19 at 18:33
  • 1
    @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_account` – 463035818_is_not_an_ai Mar 08 '19 at 18:36
  • 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. –  Mar 08 '19 at 18:40
  • 2
    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](https://stackoverflow.com/a/53051225/4342498) and [this](https://timsong-cpp.github.io/cppwp/basic.types#11) seems to back it up that it will have to be the same but I'm not sure. – NathanOliver Mar 08 '19 at 18:46
  • 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. –  Mar 08 '19 at 18:50
  • 2
    @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 them – 463035818_is_not_an_ai Mar 08 '19 at 19:15
  • "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. – Zan Lynx Jan 11 '21 at 17:23

4 Answers4

3

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));
atomsymbol
  • 370
  • 8
  • 11
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.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • I believe that there is something in the standard preventing this: the term "layout-compatible structs" (see my answer). – Jim Oldfield Jan 11 '21 at 17:10
  • @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. – supercat Jan 11 '21 at 18:00
  • 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"? – Jim Oldfield Jan 26 '21 at 00:06
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.

Jim Oldfield
  • 674
  • 3
  • 8
  • 1
    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? – 463035818_is_not_an_ai Jan 11 '21 at 18:38
  • 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 myself – 463035818_is_not_an_ai Jan 11 '21 at 18:38
  • 1
    @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)`. – Jim Oldfield Jan 26 '21 at 00:13
-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.

doron
  • 27,972
  • 12
  • 65
  • 103
  • 10
    I think we all agree that this would be the logical effect, however the question is about this being guaranteed by the standard. – JVApen Mar 08 '19 at 19:50