Search code examples
c++c++11librarieslibrary-designheader-only

Library design: allow user to decide between "header-only" and dynamically linked?


I have created several C++ libraries that currently are header-only. Both the interface and the implementation of my classes are written in the same .hpp file.

I've recently started thinking that this kind of design is not very good:

  1. If the user wants to compile the library and link it dynamically, he/she can't.
  2. Changing a single line of code requires full recompilation of existing projects that depend on the library.

I really enjoy the aspects of header-only libraries though: all functions get potentially inlined and they're very very easy to include in your projects - no need to compile/link anything, just a simple #include directive.

Is it possible to get the best of both worlds? I mean - allowing the user to choose how he/she wants to use the library. It would also speed up development, as I'd work on the library in "dynamically-linking mode" to avoid absurd compilation times, and release my finished products in "header-only mode" to maximize performance.

The first logical step is dividing interface and implementation in .hpp and .inl files.

I'm not sure how to go forward, though. I've seen many libraries prepend LIBRARY_API macros to their function/class declarations - maybe something similar would be needed to allow the user to choose?


All of my library functions are prefixed with the inline keyword, to avoid "multiple definition of..." errors. I assume the keyword would be replaced by a LIBRARY_INLINE macro in the .inl files? The macro would resolve to inline for "header-only mode", and to nothing for the "dynamically-linking mode".


Solution

  • Preliminary note: I am assuming a Windows environment, but this should be easily transferable to other environments.

    Your library has to be prepared for four situations:

    1. Used as header-only library
    2. Used as static library
    3. Used as dynamic library (functions are imported)
    4. Built as dynamic library (functions are exported)

    So let's make up four preprocessor defines for those cases: INLINE_LIBRARY, STATIC_LIBRARY, IMPORT_LIBRARY, and EXPORT_LIBRARY (it is just an example; you may want to use some sophisticated naming scheme). The user has to define one of them, depending on what he/she wants.

    Then you can write your headers like this:

    // foo.hpp
    
    #if defined(INLINE_LIBRARY)
    #define LIBRARY_API inline
    #elif defined(STATIC_LIBRARY)
    #define LIBRARY_API
    #elif defined(EXPORT_LIBRARY)
    #define LIBRARY_API __declspec(dllexport)
    #elif defined(IMPORT_LIBRARY)
    #define LIBRARY_API __declspec(dllimport)
    #endif
    
    LIBRARY_API void foo();
    
    #ifdef INLINE_LIBRARY
    #include "foo.cpp"
    #endif
    

    Your implementation file looks just like usual:

    // foo.cpp
    
    #include "foo.hpp"
    #include <iostream>
    
    void foo()
    {
        std::cout << "foo";
    }
    

    If INLINE_LIBRARY is defined, the functions are declared inline and the implementation gets included like a .inl file.

    If STATIC_LIBRARY is defined, the functions are declared without any specifier, and the user has to include the .cpp file into his/her build process.

    If IMPORT_LIBRARY is defined, the functions are imported, and there isn't a need for any implementation.

    If EXPORT_LIBRARY is defined, the functions are exported and the user has to compile those .cpp files.

    Switching between static / import / export is a really common thing, but I'm not sure if adding header-only to the equation is a good thing. Normally, there are good reasons for defining something inline or not to do so.

    Personally, I like to put everything into .cpp files unless it really has to be inlined (like templates) or it makes sense performance-wise (very small functions, usually one-liners). This reduces both compile time and - way more important - dependencies.

    But if I choose to define something inline, I always put it in separate .inl files, just to keep the header files clean and easy to understand.