3

First, just to avoid XY problem: this issue comes from https://github.com/cnjinhao/nana/issues/445#issuecomment-502080177. The library code should probably not do such thing (reliance on construction of unused global object) but the question is more about whether it's valid LTO behaviour rather than code quality issues.


Minimal code that showcases the same problem (untested, just to make example smaller):

// main.cpp
#include <lib/font.hpp>

int main()
{
    lib::font f;
}
// lib/font.hpp
namespace lib
{
struct font
{
    font();

    int font_id;
};
}
// lib/font.cpp
#include <lib/font.hpp>
#include <lib/font_abstraction.hpp>

namespace lib
{
font::font()
{
    font_id = get_default_font_id();
}
}
// lib/font_abstraction.hpp
namespace lib
{
int get_default_font_id();

void initialize_font();
}
// lib/font_abstraction.cpp
#include <lib/font_abstraction.hpp>

namespace lib
{
static int* default_font_id;

int get_default_font_id()
{
    return *default_font_id;
}

void initialize_font()
{
    default_font_id = new int(1);
}
}
// lib/platform_abstraction.hpp
namespace lib
{
struct platform_abstraction
{
    platform_abstraction();
};
}
// lib/platform_abstraction.cpp
#include <lib/platform_abstraction.hpp>
#include <lib/font_abstraction.hpp>

namespace lib
{
platform_abstraction::platform_abstraction()
{
    initialize_font();
}

static platform_abstraction object;
}

The construction of font object in main.cpp relies on the initialization of the pointer. The only thing that initializes the pointer is global object object but it's unsued - in the case of linked issue that object was removed by LTO. Is such optimization allowed? (See C++ draft 6.6.5.1.2)

Some notes:

  • The library was build as a static library and linked with main file using -flto -fno-fat-lto-objects and dynamic C++ standard library.
  • This example can be build without compiling lib/platform_abstraction.cpp at all - in such scenario the pointer will not be initialized for sure.
Xeverous
  • 973
  • 1
  • 12
  • 25
  • Isn't this just a different symptom of the [static initialization order fiasco](https://isocpp.org/wiki/faq/ctors#static-init-order)? – Henri Menke Jun 17 '19 at 09:45
  • 1
    A *minimal* example should not have seven files. – n. m. could be an AI Jun 17 '19 at 09:48
  • @n.m. This particular problem is related to code dependency accross translation units so I wanted to create the same dependency graph as it is in the library. – Xeverous Jun 17 '19 at 10:00
  • 3
    I get segfault without lto, too. `--whole-archive` fixes it, this is an issue with linking static library. When building without the static library it fixes it too. As to language-lawyer: constructor is a normal function, it can't be optmized out if it has visible side effects. – KamilCuk Jun 17 '19 at 10:00
  • @HenriMenke I dont think so. SIOF is when one static object relies on other static object being initialized first. Here the problem is not the order but whether the object will be constructed at all. – Xeverous Jun 17 '19 at 10:01

3 Answers3

5

Since you never reference object from static library in the main executable it is not going to exist unless you link that static library with -Wl,--whole-archive. It is not a good idea to rely on construction of some global objects to perform initialization anyway. So you should just invoke initialize_font explicitly prior to using other functions from that library.

Additional explanation for question tagged language-lawyer:

static platform_abstraction object; can not be eliminated in any circumstances according to

6.6.4.1 Static storage duration [basic.stc.static]
2 If a variable with static storage duration has initialization or a destructor with side effects, it shall not be eliminated even if it appears to be unused, except that a class object or its copy/move may be eliminated as specified in 15.8.

So what is going on here? When linking static library (archive of object files) by default linker will only pick objects files required to fill undefined symbols and since stuff from platform_abstraction.cpp is not used anywhere else linker will completely omit this translation unit. --whole-archive option alters this default behavior by forcing linker to link all the object files from the static library.

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • *it is not going to exist unless you link that static library with `-Wl,--whole-archive`*. So to confirm, such removal by LTO is allowed and the flag is needed to explicitly disable this optimization? – Xeverous Jun 17 '19 at 10:03
  • @Xeverous It will be removed even without LTO. I think it is a common problem with static libraries that rely on construction of some globals. – user7860670 Jun 17 '19 at 10:06
  • *It is not a good idea to rely on construction of some global objects to perform initialization anyway* Could you summarize why or link to an explanation? – nada Jun 17 '19 at 10:18
  • @nada Because it is a direct path to Static (Un)Initialization Order Fiasco, testing and debugging difficulties. To be honest I think that introduction of dynamic initialization stage was a rather poor decision. – user7860670 Jun 17 '19 at 10:49
5

VTT's answer gives a GCC answer, but the question is tagged language-lawyer.

The ISO C++ reason is that objects defined in a Translation must be initialized before the first call to a function defined in the same Translation Unit. That means platform_abstraction::object must be initialized before platform_abstraction::platform_abstraction() is called. As the linker correctly figured out, there are no other platform_abstraction objects, so platform_abstraction::platform_abstraction is never called, so object's initialization can be postponed indefinitely. A conforming program cannot detect this.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • So given this, Meyer's singleton is the best approach to both guuarantee correct order (no static initialization order fiasco) and to guuarantee that object will not be removed by the linker because it's used from a function in the same translation unit. – Xeverous Jun 18 '19 at 07:39
  • *The ISO C++ reason is that objects defined in a Translation must be initialized before the first call to a function defined in the same Translation Unit.* Can you link to standard draft for this permission or is it implied by other rules? – Xeverous Jun 18 '19 at 15:06
  • 3
    http://eel.is/c++draft/basic.start.dynamic "It is implementation-defined whether the dynamic initialization of a non-local non-inline variable with static storage duration is sequenced before the first statement of main or is deferred. ". GCC apparently defers it. – MSalters Jun 18 '19 at 15:18
0

Don't have global static variables.

The order of initialization is undefined (in the general case).

Put static objects inside funcations as static objects then you can gurantee they are created before use.

namespace lib
{
static int* default_font_id;

int get_default_font_id()
{
    return *default_font_id;
}

void initialize_font()
{
    default_font_id = new int(1);
}
}

// Change this too:

namespace lib
{

int get_default_font_id()
{
     // This new is guaranteed to only ever be called once.
     static std::unique_ptr<int> default_font_id = new int(1);

     return *default_font_id;
}

void initialize_font()
{
    // Don't need this ever.
}
}
Martin York
  • 257,169
  • 86
  • 333
  • 562