Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing C++ flavours in the same project

Is it safe to mix C++98 and C++11 in the same project? By "mixing" I mean not only linking object files but also common header files included in the source code compiled with C++98 and C++11.

The background for the question is the desire to transition at least a part of a large code base to C++11. A part of the code is in C++ CUDA, compiled to be executed on either GPU or CPU, and the corresponding compiler doesn't support C++11 at this time. However, much of the code is intended for CPU only and can be compiled with either C++ flavour. Some header files are included in both CPU+GPU and CPU-only source files.

If we now compile CPU-only source files with C++11 compiler, can we be confident against undesirable side effects?

like image 944
Michael Avatar asked Dec 05 '13 17:12

Michael


1 Answers

In practice, maybe.

It is relatively common for the standard library of C++11 and C++03 to disagree about what the layout of std namespace objects is. As an example, sizeof(std::vector<int>) changed noticeably over various compiler versions in MSVC land. (it got smaller as they optimized it)

Other examples could be a different heap on each side of the compiler fence.

So you have to carefully "firewall" between the two source trees.

Now, some compilers seek to minimize such binary compatibility changes, even at the cost of violating the standard. I believe std::list without a size counter might be an example of that (which violates C++11, but I recall that at least one vendor provided a standards-non-compliant std::list to maintain binary compatibility -- I don't remember which one).

For the two compilers (and a compiler in C++03 and C++11 are different compilers) you are going to have some ABI guarantees. There is probably a large chunk of the language for which the ABI will agree, and on that set you are relatively safe.

To be reasonably safe, you'll want to treat the other compiler version files as if they are third party DLLs (delay loaded libraries) that do not link to the same C++ standard library. That means any resources passed from one to the other have to be packaged with destruction code (ie, returned to the DLL from whence it came to be destroyed). You'll either have to investigate the ABI of the two standard libraries, or avoid using it in the common header files, so you can pass things like smart pointers between the DLLs.

An even safer approach is to strip yourself down to a C style interface with the other code base, and only pass handles (opaque types) between the two code bases. To make this sane, whip up some header-file only mojo that wraps the C style interface in pretty C++ code, just don't pass those C++ objects between the code bases.

All of this is a pain.

For example, suppose you have a std::string get_some_string(HANDLE) function, and you don't trust ABI stability.

So you have 3 layers.

namespace internal {
  // NOT exported from DLL
  std::string get_some_string(HANDLE) { /* implementation in DLL */ }
}
namespace marshal {
  // exported from DLL
  // visible in external headers, not intended to be called directly
  void get_some_string(HANDLE h, void* pdata, void(*callback)( void*, char const* data, std::size_t length ) ) {
    // implementation in DLL
    auto r = ::internal::get_some_string(h);
    callback( pdata, r.data(), r.size() );
  }
}
namespace interface {
  // exists in only public header file, not within DLL
  inline std::string get_some_string(HANDLE h) {
    std::string r;
    ::marshal::get_some_string(h, &r,
      [](void* pr, const char* str, std::size_t length){
        std::string& r = *static_cast<std::string*>(pr);
        r.append( str, length );
      }
    );
    return r;
  }
}

So the code outside the DLL does an auto s = ::interface::get_some_string(handle);, and it looks like a C++ interface.

The code inside the DLL implements std::string ::internal::get_some_string(HANDLE);.

The marshal's get_some_string provides a C-style interface between the two, which provides better binary compatibility than relying on the layout and implementation of std::string to remain stable between the DLL and the code using the DLL.

The interface's std::string exists completely within the non-DLL code. The internal std::string exists completely within the DLL-code. The marshal code moves the data from one side to the other.

like image 154
Yakk - Adam Nevraumont Avatar answered Nov 17 '22 19:11

Yakk - Adam Nevraumont