skip to Main Content

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 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


  1. 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:

    we want users to be able to build relocatable objects with different compilers and link them together, and if possible even to ship common DSOs.

    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

    Extending existing, stable ABIs. Versioning gives subsequent releases of library binaries the ability to add new symbols and add functionality, all the while retaining compatibility with the previous releases in the series. Thus, program binaries linked with the initial release of a library binary will still run correctly if the library binary is replaced by carefully-managed subsequent library binaries. This is called forward compatibility.

    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.

    Login or Signup to reply.
  2. The issue can be reproduced with this small code:

    #include <iostream>
    
    struct A { 
        double d{0.0};
        int i1{1};
    };
    
    struct C : public A { 
        int i2{2};
    };
    
    int main(int argc, char* argv[]) {
        C c;
        std::cout << "sizeof(C) = " << sizeof(C) << std::endl;
                  << "&i2 - &i1 = " << (((char*) &c.i2) - ((char*) &c.i1))
                  << std::endl;
        return 0;
    }
    

    When compiled with G++ in C++11 mode or when compiled with clang (any mode), the result is:

    sizeof(C) = 16
    &i2 - &i1 = 4
    

    When compiled with G++ in C++14 (or later) mode, the result is:

    sizeof(C) = 24
    &i2 - &i1 = 8
    

    Apparently, G++ since C++14 refuses to overlay fields of struct C into struct A. Note that sizeof(A) is 16 with all compilers, which is required, so that arrays of objects of type A are well-aligned. So there is an implicit 4-byte padidng after i1. clang (all versions) and G++ (until C++11) recycle that padding, when another int 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search