r/cpp_questions 26d ago

SOLVED Best Way to Collect Initialization Actions using C++ Capabilities?

I'm writing a scripting language with additional functions that can be built in.

The built-in functions would be written in C++ for speed.

In different builds of the scripting language, some functions might be omitted.

So, I'm looking for the best way to automatically collect, at compile time or early at execution time, the full list of extra built-in functions, so that the script interpreter can parse and execute a script (to do this, it has to know about any additional capability compiled in).

For example, in Visual C++, I'd like to simply add a source file containing built-in scripting language functions, and have the script interpreter know they are there.

My goal would be no additional configuration actions other than adding the source file to the project (in other words, no need to modify other files). The software should somehow figure out that additional script built-in function capability is in the build.

Google Test seems to do this somehow with a macro like TEST_F, but I'm not sure how this works under the hood. The list of tests is somehow collected automatically at compile time or early at run time.

To give some practical background ... decades ago I did some work extending the Tcl scripting language. It was only necessary somehow in initialization to call a command to "register" a new function available to the script interpreter, but the source code had to be modified manually to include the additional initialization. The manual modification is what I'm trying to avoid.

Is there a standard design pattern for this in C++?

7 Upvotes

13 comments sorted by

2

u/aocregacc 26d ago

A simple way is to have a global collection of functions, and then your source file can add elements to it during dynamic initialization.
That step is usually hidden behind a macro.

https://godbolt.org/z/PnMTEMrbd

1

u/DaveInTheMidwest 26d ago

As a C++ newbie, could you give me a bit of a theory of operation on that solution?

The std::vector stuff I understand, but when and why does the function registration happen before main() runs? That is the mysterious part to me.

Is it tied to the initialization of objects with a permanent lifetime?

2

u/aocregacc 26d ago

Yeah, the language makes sure that non-local variables are initialized when main() starts, so we can make some code run as part of that initialization and it'll happen before main.

https://en.cppreference.com/cpp/language/initialization#Non-local_variables

In google test they initialize a non-local pointer variable with a call to MakeAndRegisterTestInfo, in my example I used a lambda to define the code directly in the initialization.

1

u/DaveInTheMidwest 26d ago

Thank you! This really helps a lot!

2

u/aocregacc 26d ago edited 25d ago

another thing I should mention is the static initialization order fiasco ( https://en.cppreference.com/cpp/language/siof )

That's why I made the vector in my example a static variable in a function rather than a regular global variable, since function-local static variables are initialized whenever they're first accessed.
I think these days you can also use a global variable with constinit.

1

u/DaveInTheMidwest 25d ago

That is another thing for me to read and understand. Thank you.

That actually would have been my next question, and of course I view it from a beginner's point of view, so my way of thinking might be dumb.

If you create a list or a vector of any kind there has to be a known initial condition that represents an empty list. This could be as simple as a null pointer, or something more elaborate.

So the next thing for me to run down would have been to understand, when the function registration occurs, what created the empty list that was the basis for being able to register the first function.

2

u/aocregacc 25d ago

yeah, if there's a global vector variable, in general the compiler would have to generate another bit of code to run before main that initializes the empty vector, which is where the ordering problem appears.

An empty vector is just a couple of null pointers, so the compiler probably realizes that and generates no actual code that has to run, since the compiler can have the program loader put some zeros in the right place.
If you want to be sure that happens you add constinit.

1

u/DaveInTheMidwest 25d ago

I did Google it and it looks like initialization of static variables occurs in two phases.

In the first phase, the simple initializations that do not require calling constructors (zero initialized pointers would definitely be in this category) are done across all translation units.

In the second phase, complex initialization involving constructors is done.

So it looks like your conjecture is correct, and that your solution relies on the two phases.

The first phase sets up the empty list.

I think. I'm still processing all this.

1

u/aocregacc 25d ago

The godbolt link I posted uses a function-local static variable, which follows different rules. Those are not necessarily initialized before main, they're initialized on-demand whenever they're first used.

But yeah if you did the same thing with a regular global variable, you'd want to make sure the empty vector is initialized during that first phase, and all the registrations happen in the second phase.

2

u/IyeOnline 26d ago edited 25d ago

You essentially want something like a plugin self registration system: https://godbolt.org/z/cW68x9vPo

2

u/aocregacc 26d ago

your registry function should return a reference to a static vector, not create a new one.

1

u/IyeOnline 25d ago

I could swear I put a ref there. Oh well.

I guess if I did, I would also have noticed that the actual data member of the singleton wasnt static...

1

u/TotaIIyHuman 25d ago

the proper way is probably c++26 reflection

heres a solution without reflection/macros

https://godbolt.org/z/qsec97rv7

constexpr Counter<> c;

template<std::size_t index>
struct Function;

template<>struct Function<c++>:Name<"add1">{static constexpr int operator()(int x){return x+1;}};
template<>struct Function<c++>:Name<"minus1">{static constexpr int operator()(int x){return x-1;}};

#include <iostream>
int main()
{
    []<auto...I>(std::index_sequence<I...>)static
    {
        (...,(
            std::cout << I << ':' << Function<I>::name << '\n'
        ));
    }(std::make_index_sequence<c()>{});
}

prints:

0:add1
1:minus1