Using static variables and static linking of executable modules to each other

    Good Friday evening!

    Today I want to talk about some insidious features of static variables with incorrect linking of executable modules. I will show a problem from my real practice that everyone can experience.
    I chew everything in quite a bit of detail, so “experienced” and red-eyed people may have the feeling that I “bathe in the sandbox”, but this article is not only for them.

    Let's imagine a situation: there is some class implemented in a static library (lib). This library is statically bound by the implementation module (dll). Further this executable module (exe) also statically binds this dll. In addition, the Exe module statically links the static library (lib).
    Like that:

    image

    For example, there is the following logic: in lib, some tool is implemented for something. The dll has some functionality based on this tool. Exe implements a test for this functionality. Dll itself does not export the tool class (which is located in lib'e), so the test requires static linking of libs.
    Let the instrumental class contain a static variable. And in the dll there is a function for creating this class, and the object is returned by value.
    Here is an extended outline:

    image
    Here is the C ++ code:

    • lib
      ListAndIter.h
      #pragma once
      #include 
      using namespace std;
      class ListAndIter
      {
         private:
            std::list::iterator iter;
            static std::list &getList();
         public:
            void foo();
            ListAndIter();
            ListAndIter(ListAndIter& rhs);
            ~ListAndIter();      
      };
      

      ListAndIter.cpp
      #include "ListAndIter.h"
      ListAndIter::ListAndIter()
      {
          getList().push_front(0);
          iter = getList().begin();
      }
      ListAndIter::ListAndIter(ListAndIter& rhs)
      {
         this->iter = rhs.iter;
         rhs.iter = getList().end();
      }
      std::list & ListAndIter::getList()
      {
         static std::list MyList;
         return MyList;
      }
      ListAndIter::~ListAndIter()
      {
         if (iter != getList().end())
             getList().erase(iter);
      }
      void ListAndIter::foo()
      {
      }
      


    • dll
      GetStaticObj.h
      #pragma once
      #include "ListAndIter.h"
      #ifdef _DLL_EXPORTS
         #define _DLL_EXP __declspec(dllexport)
      #else
         #define _DLL_EXP __declspec(dllimport)
      #endif
      _DLL_EXP ListAndIter GetStaticObj();
      

      GetStaticObj.cpp
      #include "GetStaticObj.h"
      ListAndIter GetStaticObj()
      {
         ListAndIter obj;
         obj.foo();
         return obj;
      }
      


    • exe
      Main.cpp
      #include "GetStaticObj.h"
      int main()
      {
         ListAndIter obj = GetStaticObj();
         obj.foo();
      }
      


    As you can see from the code, there is a special function foo that serves to bypass RVO so that the copy constructor is called. Let me remind you that both the dll module and the exe module are assembled independently of each other, so they should know about the existence of a static variable in lib and therefore create them on their own.

    The object of the ListAndIter class is returned via the copy constructor, therefore, when an object is received on the side of the exe-module, all references to the static variable will become invalid. In steps, it looks like this:
    1. * .exe: Call the GetStaticObj () function.
    2. Dll.dll: creating a temporary object of the ListAndIter class. Zero is put in the list, the iter iterator points to it. Moreover, at this time, the static variable on the side of the exe-module is empty, respectively, the iterator is not valid.
    3. Dll.dll: Copy constructor is called for an object of class ListAndIter. An iterator has become invalid for a temporary object. In the new object, the iterator points to the list from the DLL.dll, although the object itself is created on the side of the exe-module.
    4. Dll.dll: Destroyed a temporary object of class ListAndIter. Since the iterator is not valid, no action occurs.
    5. * .exe: The destructor for the obj object is called. When trying to compare an iterator with getList (). End (), a Windows error pops up: "Iterators are not compatible." That is, an iterator from the “other list”.

    Let's try to fix this situation by removing the dependence of the exe-module on the static library. Then all the functionality of the static library needs to be exported via dll (see the code below):

    image
    Changes in the code:
    • Created a new shared.h header file. It describes export macros. Placed the file in lib:
      shared.h
      #pragma once
      #ifdef _DLL_EXPORTS
         #define _DLL_EXP __declspec(dllexport)
      #else
         #define _DLL_EXP __declspec(dllimport)
      #endif
      

    • Added export directives to ListAndIter.h:
      ListAndIter.h
      #pragma once
      #include 
      #include "shared.h"
      using namespace std;
      class ListAndIter
      {
         private:
            std::list::iterator iter;
            _DLL_EXP static std::list &getList();
         public:
            _DLL_EXP void foo();
            _DLL_EXP ListAndIter();
            _DLL_EXP ListAndIter(ListAndIter& rhs);
            _DLL_EXP ~ListAndIter();
      };
      

    • In the dll, I accordingly removed the export macro declarations:
      GetStaticObj.h
      #pragma once
      #include "ListAndIter.h"
      #include "shared.h"
      _DLL_EXP ListAndIter GetStaticObj();
      


    Now the object will be created and deleted only on the dll side. There will be no static variable in the exe-module and such code will work successfully.

    Now let's assume what happens if the ListAndIter class becomes a boilerplate:

    image

    There must be a static variable for each full specialization of the template and all objects of such classes.
    First of all, we are obliged to place the implementation of the template class in the header file, because templates are revealed at the compilation stage.
    If the static variable is a member of the class, then in order to successfully assemble our project, we are forced to explicitly initialize these variables in all the modules used. In this case, we explicitly create two static variables, which returns us to the 1st example.
    Otherwise, if the static variable is not a member of the class, but is created through the static method, then in this case it is also created, but already implicitly for us. The error is repeated again.

    To resolve this situation, you need to create an intermediate lib, in which to place this functionality. That is, instead of dll, do lib. Then again one static variable will remain.

    Conclusion : When using static variables in static libraries, you need to ensure that executable modules do not link statically to each other.

    Sometimes the problem is not solved by simplification of dependencies. For example, a class is implemented in a static library and it has a certain static instance counter. This static library links into two different dlls, so two different counters are created in them. In this case, the problem is solved by turning the static library into a dynamic one (dll). Accordingly, the other two dlls link the new dll dynamically. Then the static variable will be in only one dll (in the one to which the class with the counter is implemented).

    All code can be taken from github .

    PS I wrote a lot of things, maybe not perfect ... I will be glad to advice and comments.

    Also popular now: