Suppose I have three compiled objects, all produced by the same compiler/version:
For simplicity, let's assume all headers were written in C++11, using only constructs whose semantics haven't changed between all three standard versions, and so any interdependencies were correctly expressed with header inclusion and the compiler did not object.
Which combinations of these objects is it and isn't it safe to link into a single binary? Why?
EDIT: answers covering major compilers (e.g. gcc, clang, vs++) are welcome
You can only mix generated binary files from different compilers or different versions of the same compiler if they are ABI (Application Binary Interface) compatible.
C++14 is a version of the ISO/IEC 14882 standard for the C++ programming language. It is intended to be a small extension over C++11, featuring mainly bug fixes and small improvements, and was replaced by C++17. Its approval was announced on August 18, 2014. C++14 was published as ISO/IEC 14882:2014 in December 2014.
What is the difference between C++11, C++14, and C++17, and which among them is the best to use? The obvious difference is the year they were published as the official C++ ISO standard. (2011, 2014 and 2017 respectively).
3.3] constexpr member-functions, no longer implicitly const. There are many changes to constexpr in C++14, but the only change that will change semantics between C++11, and C++14 is the constantness of a member-function marked as constexpr.
Which combinations of these objects is it and isn't it safe to link into a single binary? Why?
For GCC it is safe to link together any combination of objects A, B, and C. If they are all built with the same version then they are ABI compatible, the standard version (i.e. the -std
option) doesn't make any difference.
Why? Because that's an important property of our implementation which we work hard to ensure.
Where you have problems is if you link together objects compiled with different versions of GCC and you have used unstable features from a new C++ standard before GCC's support for that standard is complete. For example, if you compile an object using GCC 4.9 and -std=c++11
and another object with GCC 5 and -std=c++11
you will have problems. The C++11 support was experimental in GCC 4.x, and so there were incompatible changes between the GCC 4.9 and 5 versions of C++11 features. Similarly, if you compile one object with GCC 7 and -std=c++17
and another object with GCC 8 and -std=c++17
you will have problems, because C++17 support in GCC 7 and 8 is still experimental and evolving.
On the other hand, any combination of the following objects will work (although see note below about libstdc++.so
version):
-std=c++03
-std=c++11
-std=c++17
This is because C++03 support is stable in all three compiler versions used, and so the C++03 components are compatible between all the objects. C++11 support is stable since GCC 5, but object D doesn't use any C++11 features, and objects E and F both use versions where C++11 support is stable. C++17 support is not stable in any of the used compiler versions, but only object F uses C++17 features and so there is no compatibility issue with the other two objects (the only features they share come from C++03 or C++11, and the versions used make those parts OK). If you later wanted to compile a fourth object, G, using GCC 8 and -std=c++17
then you would need to recompile F with the same version (or not link to F) because the C++17 symbols in F and G are incompatible.
The only caveat for the compatibility described above between D, E and F is that your program must use the libstdc++.so
shared library from GCC 7 (or later). Because object F was compiled with GCC 7, you need to use the shared library from that release, because compiling any part of the program with GCC 7 might introduce dependencies on symbols that are not present in the libstdc++.so
from GCC 4.9 or GCC 5. Similarly, if you linked to object G, built with GCC 8, you would need to use the libstdc++.so
from GCC 8 to ensure all symbols needed by G are found. The simple rule is to ensure the shared library the program uses at run-time is at least as new as the version used to compile any of the objects.
Another caveat when using GCC, already mentioned in the comments on your question, is that since GCC 5 there are two implementations of std::string
available in libstdc++. The two implementations are not link-compatible (they have different mangled names, so can't be linked together) but can co-exist in the same binary (they have different mangled names, so don't conflict if one object uses std::string
and the other uses std::__cxx11::string
). If your objects use std::string
then usually they should all be compiled with the same string implementation. Compile with -D_GLIBCXX_USE_CXX11_ABI=0
to select the original gcc4-compatible
implementation, or -D_GLIBCXX_USE_CXX11_ABI=1
to select the new cxx11
implementation (don't be fooled by the name, it can be used in C++03 too, it's called cxx11
because it conforms to the C++11 requirements). Which implementation is the default depends on how GCC was configured, but the default can always be overridden at compile-time with the macro.
There are two parts to the answer. Compatibility at the compiler level and compatibility at the linker level. Let's start with the former.
let's assume all headers were written in C++11
Using the same compiler means that the same standard library header and source files (the onces associated with the compiler) will be used irrespective of the target C++ standard. Therefore, the header files of the standard library are written to be compatible with all C++ versions supported by the compiler.
That said, if the compiler options used to compile a translation unit specify a particular C++ standard, then the any features that are only available in newer standards should not be accessible. This is done using the __cplusplus
directive. See the vector source file for an interesting example of how it's used. Similarly, the compiler will reject any syntactic features offered by newer versions of the standard.
All of that means that your assumption can only apply to the header files you wrote. These header files can cause incompatibilities when included in different translation units targeting different C++ standards. This is discussed in Annex C of the C++ standard. There are 4 clauses, I'll only discuss the first one, and briefly mention the rest.
C.3.1 Clause 2: lexical conventions
Single quotes delimit a character literal in C++11, whereas they are digit separators in C++14 and C++17. Assume you have the following macro definition in one of the pure C++11 header files:
#define M(x, ...) __VA_ARGS__ // Maybe defined as a field in a template or a type. int x[2] = { M(1'2,3'4) };
Consider two translation units that include the header file, but target C++11 and C++14, respectively. When targeting C++11, the comma within the quotes is not considered to be a parameter separator; there is only once parameter. Therefore, the code would be equivalent to:
int x[2] = { 0 }; // C++11
On the other hand, when targeting C++14, the single quotes are interpreted as digit separators. Therefore, the code would be equivalent to:
int x[2] = { 34, 0 }; // C++14 and C++17
The point here is that using single quotes in one of the pure C++11 header files can result in surprising bugs in the translation units that target C++14/17. Therefore, even if a header file is written in C++11, it has to be written carefully to ensure that it's compatible with later versions of the standard. The __cplusplus
directive may be useful here.
The other three clauses from the standard include:
C.3.2 Clause 3: basic concepts
Change: New usual (non-placement) deallocator
Rationale: Required for sized deallocation.
Effect on original feature: Valid C++2011 code could declare a global placement allocation function and deallocation function as follows:
void operator new(std::size_t, std::size_t); void operator delete(void*, std::size_t) noexcept;
In this International Standard, however, the declaration of operator delete might match a predefined usual (non-placement) operator delete (3.7.4). If so, the program is ill-formed, as it was for class member allocation functions and deallocation functions (5.3.4).
C.3.3 Clause 7: declarations
Change: constexpr non-static member functions are not implicitly const member functions.
Rationale: Necessary to allow constexpr member functions to mutate the object.
Effect on original feature: Valid C++2011 code may fail to compile in this International Standard.
For example, the following code is valid in C++2011 but invalid in this International Standard because it declares the same member function twice with different return types:
struct S { constexpr const int &f(); int &f(); };
C.3.4 Clause 27: input/output library
Change: gets is not defined.
Rationale: Use of gets is considered dangerous.
Effect on original feature: Valid C++2011 code that uses the gets function may fail to compile in this International Standard.
Potential incompatibilities between C++14 and C++17 are discussed in C.4. Since all the non-standard header files are written in C++11 (as specified in the question), these issues will not occur, so I will not mention them here.
Now I'll discuss compatibility at the linker level. In general, potential reasons for incompatibilities include the following:
main
entry point.If the format of the resulting object file depends on the target C++ standard, the linker must be able to link the different object files. In GCC, LLVM, and VC++, this is fortunately not the case. That is, the format of objects files is the same irrespective of the target standard, although it is highly dependent on the compiler itself. In fact, none of the linkers of GCC, LLVM, and VC++ require knowledge about the target C++ standard. This also means that we can link object files that are already compiled (statically linking the runtime).
If the program startup routine (the function that calls main
) is different for different C++ standards and the different routines are not compatible with each other, then it would not be possible to link the object files. In GCC, LLVM, and VC++, this is fortunately not the case. In addition, the signature of the main
function (and the restrictions that apply on it, see Section 3.6 of the standard) is the same in all C++ standards, so it doesn't matter in which translation unit it exists.
In general, WPO may not work well with object files compiled using different C++ standards. This depends on exactly which stages of the compiler require knowledge of the target standard and which stages don't and the impact that it has on inter-procedural optimizations that cross object files. Fortunately, GCC, LLVM, and VC++ are well designed and don't have this issue (not that I'm aware of).
Therefore, GCC, LLVM, and VC++ have been designed to enable binary compatibility across different versions of the C++ standard. This is not really a requirement of the standard itself though.
By the way, although the VC++ compiler offers the std switch, which enables you to target a particular version of the C++ standard, it does not support targeting C++11. The minimum version that can be specified is C++14, which is the default starting from Visual C++ 2013 Update 3. You could use an older version of VC++ to target C++11, but then you would have to use different VC++ compilers to compile different translation units that target different versions of the C++ standard, which would at the very least break WPO.
CAVEAT: My answer may not be complete or very precise.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With