constinit

Definition: Initialization Of Globals

  • static locals

    • Initialized when program flow crosses initialization the first time

  • Globals

    • Initialization in definition order per compilation unit

    • Initialization order of compilation units is undefined

  • Since 1970-01-01: integrals are initialized at compile time

Problem: The Static Initialization Order Fiasco

#pragma once

class point
{
public:
    point(int x, int y) : _x{x}, _y{y} {}
    int x() const { return _x; }
    int y() const { return _y; }
private:
    int _x, _y;
};
#pragma once

#include "point.h"

extern point global_point_standalone;
#include "global-point-standalone.h"

point global_point_standalone{42,666};
#pragma once

#include "point.h"

extern point global_point_depends;
#include "global-point-depends.h"

#include "global-point-standalone.h"

point global_point_depends{global_point_standalone};
#include "global-point-standalone.h"
#include "global-point-depends.h"

#include <iostream>

std::ostream& operator<<(std::ostream& o, const point& p)
{
    o << '(' << p.x() << ',' << p.y() << ')';
    return o;
}

int main()
{
    std::cout << "global_point_standalone: " << global_point_standalone << std::endl;
    std::cout << "global_point_depends: " << global_point_depends << std::endl;
    return 0;
}
  • Compile

$ g++ -c -o global-point-depends.o global-point-depends.cpp
$ g++ -c -o global-point-standalone.o global-point-standalone.cpp
$ g++ -c -o main.o main.cpp
  • Link order wrong: “standalone” after “depends”

$ g++ main.o global-point-depends.o global-point-standalone.o
$ ./a.out
global_point_standalone: (42,666)
global_point_depends: (0,0)
  • Link order wrong: “standalone” before “depends”

$ g++ main.o global-point-standalone.o global-point-depends.o
$ ./a.out
global_point_standalone: (42,666)
global_point_depends: (42,666)

Solution: constinit

The Problem

  • std::mutex is initialized at runtime

  • unsigned is initialized at compiletime (integral)

  • this is asymmetric

  • Want ``std::mutex`` to be initialized at compiletime!

  • This should be possible because it has a constexpr default constructor

#include <thread>
#include <mutex>
#include <chrono>
#include <iostream>

unsigned sequence;
std::mutex sequence_lock;                              // <-- initialized at runtim

int main()
{
    auto use_sequence = [](const char* name) {
        using namespace std::chrono_literals;
        while (true) {
            sequence_lock.lock();
            unsigned num = ++sequence;
            sequence_lock.unlock();
            std::cout << name << ": " << num << '\n';
            std::this_thread::sleep_for(500ms);
        }
    };

    std::thread t1(use_sequence, "thread 1");
    std::thread t2(use_sequence, "thread 2");

    t1.join();
    t2.join();

    return 0;
}

Possible Solutions?

  • Make sequence_lock a constexpr object

    • No: lock() and unlock() are not const

  • Live with runtime initialization

  • constinit

    • Yay

Solution: constinit

#include <thread>
#include <mutex>
#include <chrono>
#include <iostream>

constinit unsigned sequence;                           // <-- compiletime anyway, even without constinit
constinit std::mutex sequence_lock;                    // <-- compiletime

int main()
{
    auto use_sequence = [](const char* name) {
        using namespace std::chrono_literals;
        while (true) {
            sequence_lock.lock();
            unsigned num = ++sequence;
            sequence_lock.unlock();
            std::cout << name << ": " << num << '\n';
            std::this_thread::sleep_for(500ms);
        }
    };

    std::thread t1(use_sequence, "thread 1");
    std::thread t2(use_sequence, "thread 2");

    t1.join();
    t2.join();

    return 0;
}