c++gccdllconstructorlinker

When will a global variable export to the executable?


Suppose I have a library (shared or static) with two files: lib.h and lib.cpp.

lib.h:

#pragma once

struct Trigger {
  struct TriggerImpl{
    TriggerImpl();
  };
  inline static TriggerImpl trigger;
};

lib.cpp:

#include "lib.h"
#include <iostream>

struct LogConsole {
  LogConsole() { std::cout << "LogConsole constructor" << std::endl; };
};

LogConsole log_console;

Trigger::TriggerImpl::TriggerImpl() {
  std::cout << "Trigger constructor" << std::endl;
}

Then I link the library to an executable file.

main.cpp:

#include <iostream>
// #include "lib.h" ///< uncomment this line and you will get different result!!!

int main() {
  std::cout << "Done." << std::endl;
  return 0;
}

With static link (e.g. CMake target_link_library(... STATIC ...)), you will get:

Done.

With dynamic linking (e.g. CMake target_link_library(... SHARED ...)), you will get:

Trigger constructor
LogConsole constructor
Done.

However, if you uncomment the #include line, and use static link, you will get:

Trigger constructor
LogConsole constructor
Done.

My questions are:

  1. Why do dynamic linking and static linking will (or, will not) call the constructors of the global variables?

  2. I can understand that, including the header will initialize trigger, but why will log_console also be initialized? What is the internal mechanism of the compiler/linker?

Possible relative links are:

  1. Language Linkage

  2. Storage Duration

But I think this might be a "FEATURE" of some particular toolchain (what I'm using is: gcc 14.1.1 on my x64 desktop with Manjaro Linux).


Solution

  • Both trigger and log_console are initialized if and only if lib.cpp is actually linked into the process.

    When you build lib.cpp into a shared library, and link the main executable directly with that shared library, then the dynamic loader loads shared library into your process and initializes it (before the first instruction of the executable runs).

    When you build lib.cpp into an archive library, what happens depends on whether the static linker selects lib.o into the link or not.

    The linker will only select lib.o into the link if there is some reason for it to do so. See this post for full details.

    When you #include "lib.h", there is indeed such a reference from main.o to lib.o, and the linker pulls lib.o into the main executable.

    When you comment out // #include "lib.h", there is nothing in main.o that suggests to the linker that lib.o is needed, so it skips that object.

    You can observe this in action:

    You could also examine nm main.o for both cases and observe that with #include there is an unresolved trigger symbol, and with it commented out there is not.

    P.S. The linker only gets to decide on whether to link lib.o into the binary or not if lib.o is in an archive library. If lib.o or lib.cpp is listed directly on command line, then the linker doesn't get a choice and must include lib.o (which is why selbie`s experiment failed to reproduce the observed behavior).