1

I want to implement a self-register pattern in c++. My goal is to have one file defining a class and the class can self-register to a registry. My implmentation goes like this

https://godbolt.org/z/qGT7sfWdK

#include <functional>
#include <memory>
#include <span>
#include <string>
#include <vector>
#include <iostream>

class ToolBase
{
public:
    virtual ~ToolBase(){}

    virtual std::vector<std::string> GetToolMenuItem() = 0;
};

std::vector<std::function<std::unique_ptr<ToolBase>()>>& GetRegistry();

std::vector<std::function<std::unique_ptr<ToolBase>()>>& GetRegistry()
{
    static std::vector<std::function<std::unique_ptr<ToolBase>()>> registered;
    return registered;
}

class ActualTool : public ToolBase
{
public:
    ActualTool(){(void)_register;} // guess this is not really needed for non-template 
register method
    ~ActualTool() override {};

public:
    std::vector<std::string> GetToolMenuItem() override
    {
        return {"Something"};
    }

    static bool _register;
    static bool Register()
    {
        GetRegistry().push_back([](){return std::unique_ptr<ToolBase>(new ActualTool());});
        return true;
    }
};

bool ActualTool::_register = ActualTool::Register();

int main(){

    std::cout << GetRegistry().size();
}

It works fine on godbolt (I assume it's a single translation unit), but my design goal is to have a standalone cpp file of the ActualTool which is exactly like what gtest does. I have read gtest's source code and see it's using a very similar method. But in my implementation (separate cpp files) I can't have _register initialized. I am not sure what I'm missing here

Update: Here is how to reproduce it in a fresh environment. I am using Apple Clang 14.0.3


// main.cpp - executable
#include <iostream>
#include "Editor.hpp"

int main(int argc, const char * argv[]) {
    Editor e;
    e.Print(); // expect to print 1, but 0
}

// the following files are compiled into a static lib which is linked with the executable

// Tool.hpp
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include <iostream>
class Tool
{
public:
    virtual ~Tool(){}

    virtual std::vector<std::string> GetToolMenuItem() = 0;
};

std::vector<std::function<std::unique_ptr<Tool>()>>& GetRegistry();

// Tool.cpp
#include "Tool.hpp"

std::vector<std::function<std::unique_ptr<Tool>()>>& GetRegistry()
{
    static std::vector<std::function<std::unique_ptr<Tool>()>> registry;
    return registry;
}

// ActualTool.cpp
#include "Tool.hpp"

class ActualTool : public Tool
{
public:
    ActualTool(){(void)_register;} // guess this is not really needed for non-template register method
    
    ~ActualTool() override {};

public:
    std::vector<std::string> GetToolMenuItem() override
    {
        return {"Something"};
    }

    static bool _register;
    static bool Register()
    {
        GetRegistry().push_back([](){return std::unique_ptr<Tool>(new ActualTool());});
        return true;
    }
};

bool ActualTool::_register = ActualTool::Register();

// Editor.hpp
#pragma once
#include <stdio.h>
#include "Tool.hpp"

class Editor
{
public:
    void Print()
    {
        std::cout << GetRegistry().size();
    }
};


Jiehong Jiang
  • 75
  • 1
  • 7
  • 1
    It's probably unrelated to multiple files. My crystal ball says, in your actual code the derived class is probably a template, and static variables in those are not instantiated if unused. – HolyBlackCat Jul 28 '23 at 12:02
  • This is standard problem. Liker builds dependency graph and removes detached parts of this graph. You need to convince linker, that this symbol `ActualTool::_register` can be used by ways linker can't predict. Try add `[[maybe_unused]]` (didn't test it but should resolve issue). For example gtest uses macro `GTEST_ATTRIBUTE_UNUSED_` which is bonded differently for each compiler for example `__attribute__((unused))`. – Marek R Jul 28 '23 at 12:16
  • What does "I can't have _register initialized" mean, exactly? Can you be more specific. – Sam Varshavchik Jul 28 '23 at 12:16
  • @MarekR I actually tried to define _register as a global variable. It does't work either in the first place, but when I defined an `extern bool _register` in another cpp file the `_register` is initialized. However the extra declaration isn't desiable. I will try keeping looking at what gtest does hopefully see the magic it's using – Jiehong Jiang Jul 28 '23 at 12:18
  • @SamVarshavchik It means `ActualTool::Register()` is not called. – Jiehong Jiang Jul 28 '23 at 12:19
  • https://github.com/google/googletest/blob/40412d85124f7c6f3d88454583c4633e5e10fc8c/googletest/include/gtest/internal/gtest-port.h#L753-L757 https://github.com/google/googletest/blob/40412d85124f7c6f3d88454583c4633e5e10fc8c/googletest/include/gtest/internal/gtest-internal.h#L1544 – Marek R Jul 28 '23 at 12:23
  • Then you need to provide a [mre] that anyone can simply cut/paste, ***exactly as shown***, compile, run, and reproduce this issue. There's nothing wrong with the shown code, as is. One possible explanation is the well-known Static Initialization Order Fiasco, but without a [mre] it cannot be stated authoritatively, as the answer. – Sam Varshavchik Jul 28 '23 at 12:28
  • I think it's not about translation unit. I tested on my xcode it works with separate cpp files. – Jiehong Jiang Jul 28 '23 at 12:30
  • Thanks guys. As @SamVarshavchik said there is nothing wrong with the shown code. I need to dig out more information. I tried unused attribute It's not working, guess it's not the actual problem – Jiehong Jiang Jul 28 '23 at 12:31
  • 1
    What editor someone uses: Xcode, notepad, vi, or emacs, is utterly irrelevant. Xcode does not test anything, on its own. The only possible variation in observed behavior from well-formed C++ code would be due to compiler differences. Xcode is not a C++ compiler, itself. – Sam Varshavchik Jul 28 '23 at 12:34
  • @SamVarshavchik sorry for being unprofessional. I want to mean that I test it on another environment which produce a different result. I have updated the description and question – Jiehong Jiang Jul 28 '23 at 13:09
  • 1
    When you link with a static library only those modules from the static library that are referenced from the code get included in the compiled and linked executables. That's how static libraries work. Nothing in `main.cpp` references any unresolved symbol in `ActualTool.cpp`, hence it does not get linked into the executable. This is clearly shown in a debugger -- `ActualTool` is not included in the executable. You want to use shared libraries, instead of static libraries, to get the behavior you want. The End. – Sam Varshavchik Jul 28 '23 at 13:25
  • 1
    TL;DR `-ltools` => `-Wl,-whole-archive -ltools -Wl,-no-whole-archive` (assuming your static lib is called libtools.a) – n. m. could be an AI Jul 28 '23 at 13:31

0 Answers0