Storing pointers to static variables

297 views Asked by At

I've created a unit test framework for c++ which I want to port to C later and I've come across a problem where a unit test simply won't run. Unit tests are created in .cpp files and only one .cpp file should run all the tests.

To simplify a bit, this is how a test is typically created:

main.cpp

#define UNIT_TEST_IMPL // or whatever
#include "unit_test.hpp"

int main()
{
    for(const auto& test : tests_queue)
        test->run();
    return 0;
}

unit_test.hpp

#pragma once

struct Base
{
protected:
    Base() = delete;
    Base(Base* ptr);

public:
    virtual void run() = 0;
};

#if defined(UNIT_TEST_IMPL)

#include <vector>

std::vector<Base*> tests_queue;

Base::Base(Base* ptr)
{
    tests_queue.push_back(ptr);
}

#endif

test.cpp

#include "unit_test.hpp"

#include <iostream>

struct Test : Base
{
    Test()
        : Base(this)
    {}

    void run() override
    {
        std::cout << "new test" << std::endl;
    }

};

struct Test2 : Base
{
    Test2()
        : Base(this)
    {}

    void run() override
    {
        std::cout << "new test2" << std::endl;
    }
};

static Test test;
static Test2 test2;

The question is: why is it not running the tests defined in test.cpp (if I create tests in the main.cpp file they run perfectly fine)? My guess is that the problem lies in the way I am storing Base pointers but I don't know. The compiler is g++ 6.4.0

2

There are 2 answers

2
Jarod42 On

static-initialization-order-fiasco in action:

Initialization order of global across translation units is unspecified, so if test, test2 are instantiated before tests_queue, the later initialization would destroy the registration.

One possible correction:

#pragma once

struct Base
{
protected:
    Base();

public:
    virtual ~Base() = default;
    virtual void run() = 0;
};

#if defined(UNIT_TEST_IMPL) // Should be defined only once.

#include <vector>

std::vector<Base*>& get_tests_queue()
{
    static std::vector<Base*> tests_queue;
    return tests_queue;
}

Base::Base()
{
    get_tests_queue().push_back(this);
}

#endif

so your main.cpp would be:

#define UNIT_TEST_IMPL // or whatever
#include "unit_test.hpp"

int main()
{
    for(const auto& test : get_tests_queue())
        test->run();
}

Your unitTests would be unmodified:

#include "unit_test.hpp"

#include <iostream>

struct Test : Base
{
    void run() override { std::cout << "new test" << std::endl; }
};

struct Test2 : Base
{
    void run() override { std::cout << "new test2" << std::endl; }
};

static Test test;
static Test2 test2;

Demo

0
Gian Paolo On

My bets go to some form of Static Initialization Fiasco

Not sure about it, but you cannot count on global variables (your Test1 and Test2 variables defined in test.cpp being initalized befor Main runs.

As a rule of thumb avoid global variables if you can (and in this case, you probably can).

But mainly, your program should not make any assumption on initialization order of global/static variables. Here you are assuming the two globals are initialized before main. Possibly, they are not