Declaring a method in a module, but Defining it in a project file

Just looking for a sanity check whether this is a bad idea.

I have a JUCE user module I developed for use in all my projects, which contains a struct called ProductConfig. It mostly holds a bunch of Strings, to store configuration values like product name, product ID, various URLs, etc. However, there are a couple methods in the struct that I want to have implemented differently on a per-project basis.

One approach is to declare the method in the struct, but not define it there, nor anywhere else in the JUCE user module. Instead, it gets defined in a project cpp file (e.g. in Main.cpp for a standalone app).

This works, and the only drawback so far is that if I fail to define the method in the project somewhere, and then call the method, I get a linker error when I try to build the project. Easy to fix though if that happens. And if a particular project doesn’t need that method, and it never gets called, then it doesn’t matter that it was never defined.

So again, in the JUCE module, I’ve got:

struct ProductConfig
{
    // ...bunch of other stuff...
    String getSecretString();
}

And then in a standalone app’s Main.cpp I’ve got:

String ProductConfig::getSecretString()
{
    // ...generate secretString...
    return secretString;
}

Is this going to bite me in the ass down the line? Is it confusing or just a sloppy approach to coding? Or is a fine solution to this problem?

A better approach might be to declare the method pure virtual and then inherit from your struct in each project. That way get a compiler error if you don’t implement the method.

Lambdas!

struct ProductConfig
{
    // ...bunch of other stuff...
    static std::function <String ()> getSecretString;
}

And then as early as it needs to be in your plug-in instantiation:

if (ProductConfig::getSecretString == nullptr) // this prevents multiple initializations
    ProductConfig::getSecretString = [] { return "Secret String"; };

@adamski I considered subclassing (substructing?) the struct, but then I lose some of the cleanliness of using a JUCE user module for this… currently I don’t have to #include "ProductConfig.h" in every project header file because I have it in a module, but I would have to #include "ProductConfigSubclass.h" in every file if I inherited from the struct.

@yfede That’s great, thank you. I had tried to figure out how I might use std::function for this, but I couldn’t get the syntax right.

I am curious however about the advantage you see of taking this route vs what I initially described. Either way, you get the same “Undefined symbols” linker error if you forget to define the function before calling it.

I suppose the programmer’s intent is clearer using std::function, that it’s just a wrapper that will need to be filled, rather than a function that’s ready to go…?

Actually, if you forget to define a std::function before calling it you a) get a crash if its nullptr or b) if you check for nullptr before calling you can add an assertion to remind yourself to add the function :slight_smile:

I’d also prefer this solution to the virtual method solution I mentioned.

However the chance to change that function at runtime makes it probably vulnerable. Not sure if it makes much of a difference, didn’t crack plugins so far.

I guess ideally you want that code to be inlined in several places, so the hacker needs to spot a lot of places to fix.

Speaking for myself, I’ll say that I get an “Undefined symbols” linker error if I call getSecretString() without defining it (in Xcode), using the std::function approach as shown by @yfede above. It doesn’t crash, because it doesn’t build.

Not really: with your solution, you get a link-time error which, even if not as early as a compilation error, still prevents you to build a launchable executable, which is a good thing in your case because you get reminded to implement that method if you need it.

With lambdas, if you call one that’s not been initialised, you will very likely cause a crash, and you won’t notice that until runtime.
To avoid that you should always check at runtime if the lambda has been initialised or not.
Or you could initialise your lambda with a default that returns an empty String, if that suits your use case:

std::function <String ()> ProductConfig::getSecretString { [] () -> String { return String (); }  };

But if it’s for copy protection reasons, I think @daniel has a point when he says that a desirable goal is to have more copies of your function sprinkled around by inlining… Perhaps to obtain that you can explore constexpr, but on that subject I’m not an expert really

The idea is to have a lambda stored in the module like

std::function<String()> getSecret;

And the module will supply that:

module.getSecret = [] { return "Buh!"; };

That will compile and if getSecret is nullptr it will crash if called

EDIT: you beat me to it @yfede :slight_smile:

1 Like

@daniel I’m not much of a reverse-engineer, so not sure how much vulnerability using a std::function adds.

Good point about the inlining, but it seems for that I’d need to subclass the ProductConfig as @adamski suggested. When I declare the ProductConfig::getSecretString() as inline, then it throws another “Undefined symbols” linker error if I don’t include the implementation.

This got me an idea: templates aren’t instantiated unless used, so I’ve tried this in Compiler Explorer:

This code compiles without errors or warnings:

struct ProjectInfo
{
    // Other stuff here
    template <int DummyValue>
    inline static int getSecret ();
    // switched to returning int for simplicity, should work with String too
};

And as soon as I add the following, I get a warning at the line where I use getSecret that tells me I’m using an uninitialised inline function. Could that be enough for you?

int main ()
{
    ProjectInfo::getSecret <0> ();
}

EDIT: To provide your implementation, write a template specialization like:

template <>
int ProjectInfo::getSecret <0> ()
{
    return 1234; // the secret
}
1 Like

That’s very clever. Since it’s all for the sake of allowing inline for an undefined function, and creates more confusion and work for programmers (myself included) to read the code, I’d want to make sure that the compiler was actualy following the request to inline the function (since “inlining is only a request to the compiler, not a command”).