I declare a struct in a header file. I use this struct in two compilation units. One is compiled with -std=c++11 and the other with -std=c++20. The sizeof my struct is different in the two compilation units.
Is it allowed to mix any -std=c++20 and -std=c++11 code or am I using GCC the wrong way?
These answers do not tell if it’s expected to work.
- I’m not using two different version of « the same » compiler (Can you mix c++ compiled with different versions of the same compiler).
- I’m not mixing two incompatible versions of the standard in my code, as far as I know (Mixing different C++ standards with GCC).
- I’m not using libraries, including standard libraries (except for displaying the results, but that’s not where the problem lies) (Is it safe to link C++17, C++14, and C++11 objects).
I’m using GCC (Debian 10.2.1-6 (Buster)) 10.2.1 20210110 on Debian 11 (Bullseye). I wonder if this version of GCC has a bug.
Code demonstrating the problem:
File 1.h
#ifndef DEF1
#define DEF1
#include "c.h"
void f1(C* c);
C* g1();
#endif
File 2.h
#ifndef DEF2
#define DEF2
#include "c.h"
void f2 (C* c);
C* g2();
#endif
File 1.cpp
#include "c.h"
#include <stdio.h>
void f1 (C* c)
{
printf("sizeof(C)=%ldn", sizeof(C));
printf("i2=%dn", c->i2);
printf("&i2-&i1=%ldn", ((char*)&c->i2) - ((char*)&c->i1));
}
C* g1()
{
C* c = new C;
c->i2 = 1;
return c;
}
File 2.cpp
#include "c.h"
#include <stdio.h>
void f2 (C* c)
{
printf("sizeof(C)=%ldn", sizeof(C));
printf("i2=%dn", c->i2);
printf("&i2-&i1=%ldn", ((char*)&c->i2) - ((char*)&c->i1));
}
C* g2()
{
C* c = new C;
c->i2 = 2;
return c;
}
File c.h
#ifndef CDEF
#define CDEF
#include <stdint.h>
struct A
{
double d1{0.0};
double d2{0.0};
double d3{0.0};
double d4{0.0};
double d5{0.0};
double d6{0.0};
double d7{0.0};
int32_t i1{};
};
#ifdef DER
struct C : public A
{
int32_t i2{};
};
#else
struct C
{
double d1{0.0};
double d2{0.0};
double d3{0.0};
double d4{0.0};
double d5{0.0};
double d6{0.0};
double d7{0.0};
int32_t i1{};
int32_t i2{};
};
#endif
#endif
File main.cpp
#include "c.h"
#include "1.h"
#include "2.h"
int main()
{
f1(g2());
f2(g1());
}
File bar.sh
g++ -std=c++20 -DDER -c 2.cpp
g++ -std=c++11 -DDER -c 1.cpp
g++ -std=c++20 -DDER -c main.cpp
g++ -std=c++20 2.o 1.o main.o -o prog
./prog
Executing ./bar.sh
sizeof(C)=64
i2=0
&i2-&i1=4
sizeof(C)=72
i2=0
&i2-&i1=8
2
Answers
Yes it is allowed, modulo bugs in GCC.
The compiler
GCC follows the Itanium ABI, which is actually platform-independent, despite the name. Here is the Itanium ABI mission statement:
Note there are no separate ABI specifications for separate versions of the C++ standard. There is one specification that works for them all.
The library
Here is the mission statement of libstdc++ as far as versioning is concerned
The library supports not one, but two different ABIs. There was a change in the C++11 standard that necessitated an ABI split. However, as the documentation points out, the choice of ABI to use is independent of the
-std
option used to compile your code. This is ancient history however. You are going to use the new C++11-compatible ABI, which is the default, about 100% of the time, unless you need to maintain an old piece of software built with pre-C++11-compatible ABI.The real life
The open source ecosystem has zillions of C++ libraries that are used in all kind of products. No one coordinates
-std
option between maintainers of different libraries. Everybody upstream uses what they want/need, and downstream the libraries are built with whatever options are there, and linked together with no problem. It all just works.I personally run Gentoo, which is a rolling release distro. I fetch whatever stable release of a software component is available directly from that library’s GitHub or whatever it is stored, and compile with whatever compiler version I currently have. I can recompile any library using any compiler version at any time. The system still works just fine. Without this kind of cross-standard, cross-version compatibility, a rolling release would never ever have a chance to work.
Conclusion
Is it 100% safe? You decide. There are compiler bugs in this area (you have found one) and sometimes people get biten by them. Then again, there are compiler bugs in all areas, but people still use compilers.
The issue can be reproduced with this small code:
When compiled with
G++
in C++11 mode or when compiled withclang
(any mode), the result is:When compiled with
G++
in C++14 (or later) mode, the result is:Apparently,
G++
since C++14 refuses to overlay fields ofstruct C
intostruct A
. Note thatsizeof(A)
is 16 with all compilers, which is required, so that arrays of objects of typeA
are well-aligned. So there is an implicit 4-byte padidng afteri1
.clang
(all versions) andG++
(until C++11) recycle that padding, when anotherint
is added through inheritance,G++
(later versions) does not.This is clearly a binary compatibility breaking change between C++11 and C++14 mode in
G++
, that can manifest itself in many places of custom code, so linking mixed-compiled code is very dangerous, if the oldest version used is C++11.