Search code examples
c++static-librariesstatic-linking

Does the usage of header only libraries with different versions result in UB


Lets assume I have a library somelib.a, that is distributed as binary by the package manager. And this library makes use of the header only library anotherlib.hpp.

If I now link my program against somelib.a, and also use anotherlib.hpp but with a different version, then this can result in UB, if somelib.a uses parts of the anotherlib.hpp in its include headers.

But what will happen if somelib.a will reference/use anotherlib.hpp only in its cpp files (so I don't know that it uses them)? Will the linking step between my application and somelib.a ensure that somelib.a and my application will both use their own version of anotherlib.hpp.

The reason I ask is if I link the individual compilation units of my program to the final program, then the linker removes duplicate symbols (depending on if it is internal linkage or not). So a header only library is normally written in a way that removing duplicate symbols can be done.

A minimal example

somelib.a is build on a system with nlohmann/json.hpp version 3.2

somelib/somelib.h

namespace somelib {
  struct config {
    // some members
  };

  config read_configuration(const std::string &path);
}

somelib.cpp

#include <nlohmann/json.hpp>


namespace somelib {
  config read_configuration(const std::string &path)
  {
     nlohmann::json j;
     std::ifstream i(path);

     i >> j;

     config c;

     // populate c based on j

     return c;
  }
}

application is build on another system with nlohmann/json.hpp version 3.5 and 3.2 and 3.5 are not compatible, and then application is then linked against the somelib.a that was build on the system with version 3.2

application.cpp

#include <somelib/somelib.h>
#include <nlohmann/json.hpp>
#include <ifstream>

int main() {
   auto c = somelib::read_configuration("config.json");

   nlohmann::json j;
   std::ifstream i("another.json");

   i >> j;

   return 0;
}

Solution

  • It hardly makes any difference that you are using a static library.

    The C++ standard states that if in a program there is multiple definitions of an inline function (or class template, or variable, etc.) and all the definitions are not the same, then you have UB.

    Practically, it means that unless the changes between the 2 versions of the header library are very limited you will have UB. For instance, if the only changes are whitespace changes, comments, or adding new symbols, then you will not have undefined behavior. However, if a single line of code in an existing function was changed, then it is UB.

    From the C++17 final working draft (n4659.pdf):

    6.2 One-definition rule

    [...]

    There can be more than one definition of a class type (Clause 12), enumeration type (10.2), inline function with external linkage (10.1.6), inline variable with external linkage (10.1.6), class template (Clause 17), non-static function template (17.5.6), static data member of a class template (17.5.1.3), member function of a class template (17.5.1.1), or template specialization for which some template parameters are not specified in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements.

    Given such an entity named D defined in more than one translation unit, then

    • each definition of D shall consist of the same sequence of tokens; and

    • in each definition of D, corresponding names, looked up according to 6.4, shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload resolution (16.3) and after matching of partial template specialization (17.8.3), except that a name can refer to (6.2.1)

      • a non-volatile const object with internal or no linkage if the object

        • has the same literal type in all definitions of D, (6.2.1.2)

        • is initialized with a constant expression (8.20),

        • is not odr-used in any definition of D, and

        • has the same value in all definitions of D,

      or

      • a reference with internal or no linkage initialized with a constant expression such that the reference refers to the same entity in all definitions of D; and (6.3)
    • in each definition of D, corresponding entities shall have the same language linkage; and

    • in each definition of D, the overloaded operators referred to, the implicit calls to conversion functions, constructors, operator new functions and operator delete functions, shall refer to the same function, or to a function defined within the definition of D; and

    • in each definition of D, a default argument used by an (implicit or explicit) function call is treated as if its token sequence were present in the definition of D; that is, the default argument is subject to the requirements described in this paragraph (and, if the default argument has subexpressions with default arguments, this requirement applies recursively).28

    • if D is a class with an implicitly-declared constructor (15.1), it is as if the constructor was implicitly defined in every translation unit where it is odr-used, and the implicit definition in every translation unit shall call the same constructor for a subobject of D.

    If D is a template and is defined in more than one translation unit, then the preceding requirements shall apply both to names from the template’s enclosing scope used in the template definition (17.6.3), and also to dependent names at the point of instantiation (17.6.2). If the definitions of D satisfy all these requirements, then the behavior is as if there were a single definition of D. If the definitions of D do not satisfy these requirements, then the behavior is undefined.