DLLs exposing a C++ interface are highly constraining. In fact, both the DLLs and the EXEs using them must be built with the same C++ compiler version, using the same settings (e.g. same _HAS_ITERATOR_DEBUGGING/_ITERATOR_DEBUG_LEVEL settings), and both the DLLs and the EXEs must be dynamically linked to the same flavor of the CRT.
For example, suppose that MyLib.DLL is a DLL exposing a C++ interface, and in particular, as part of this C++ interface, it exposes an STL class, for instance: a std::vector<int>.
Moreover, let’s suppose we have MyProgram.EXE, which uses the aforementioned MyLib.DLL. Initially both MyLib.DLL and MyProgram.EXE are built with VS2010, both dynamically linked to the VS2010’s C/C++ runtime. Everything’s fine.
Then, someone decides to improve the implementation code of MyLib.DLL, maybe using some new C++11 features, and this DLL gets rebuilt using VS2015. MyProgram.EXE is still the old VS2010-compiled executable. Now things start going wrong. What’s the matter with that?
Well, one of the problems here is that the “std::vector<int>” exposed at the interface by the VS2015-rebuilt MyLib.DLL is different from the “std::vector<int>” expected by the older MyProgram.EXE!
In fact, just considering the size in bytes of the VS2010’s “std::vector<int>” vs. the VS2015’s one, in a release build sizeof will return 16 (bytes) for the former, and 12 (bytes) for the latter. So, the std::vector’s size has changed from VS2010 to VS2015.
The reduction in size is an effect of an optimization that took place starting with VS2012: basically they avoided the storage of empty allocators for std::vector, so you end up with just three pointers in a release-mode std::vector. Considering 4 bytes for each pointer (in 32-bit builds), you end up with:
3 [pointers] *4 [bytes/pointer] = 12 bytes
which is optimally small for std::vector.
Anyway, the point is that just these two different std::vector’s sizes in VS2010 and VS2015 show that the binary layout of the std::vector template has changed between the two C++ compiler versions. So there is a clear mismatch between the std::vector expected by a DLL built with one version of the VC++ compiler and the std::vector expected by an EXE built with a different version of the VC++ compiler.
In this MSDN document titled “Breaking Changes in Visual C++ 2015”, it’s clearly written (emphasis mine):
“Standard Template Library
To enable new optimizations and debugging checks, the Visual Studio implementation of the C++ Standard Library intentionally breaks binary compatibility from one version to the next. Therefore, when the C++ Standard Library is used, object files and static libraries that are compiled by using different versions can’t be mixed in one binary (EXE or DLL), and C++ Standard Library objects can’t be passed between binaries that are compiled by using different versions.”
Note that even in case of the same VC++ compiler version, there are different std::vector’s sizes between debug builds vs. release builds. For example, with VS2015, “sizeof(std::vector<int>)” returns 16 (bytes) and 12 (bytes) in debug mode vs. release mode respectively. The size overhead in debug builds is due to some additional machinery (and consequent overhead) that helps spotting bugs in that building mode.
So, even when the same VC++ compiler is used, there are differences between std::vector’s layouts between debug builds and release builds; so, again, there’s a mismatch between the EXE’s and DLL’s expectations on a “std::vector<int>”.
There are a few options to increase the decoupling between DLLs and EXEs.
One option is to develop DLLs exposing a pure C interface. Of course, C++ can be used inside the DLL, in the implementation. But the interface must be pure C. Note that C++-specific features like exceptions must be caught inside DLL’s boundaries, and converted to something C-style like error return codes at the DLL’s boundaries. This approach is used by many Win32 APIs.
And in fact, for example, hypothetically assuming they built Windows 7 using some version of the Visual C++ 2008 compiler, we can call the Win32 APIs exposed by Windows 7 from C++ executables built using future versions of the MSVC compiler, like those shipping in Visual Studio 2010 or 2013, just to name a few. There’s no constraint for application developers in using the same C++ compiler used by the Windows Team to build the operating system, thanks to Win32 APIs exposing a pure-C interface.
Another option is to expose C++ abstract interfaces (i.e. C++ classes that contain only pure virtual methods and no data members) and C-interface helper functions, like factory functions. This is what basically COM does. And COM is another technology used by several important Windows subsystems, like DirectX. So if you develop a COM DLL, it can be safely used by C++ executables built with different versions of the VC++ compiler.
Note: There are even other details to consider when building highly reusable software components in DLLs. For example, in the presence of dynamically-allocated objects exchanged between the DLL and the EXE, the exported component and all the modules using it must use the same memory allocator. In other words, the code that allocates memory and the code that frees it must use the same allocator. An option to solve this problem is to allocate and release memory invoking APIs like CoTaskMemAlloc() and CoTaskMemFree(), since both use a common memory allocator.