Search code examples
c++templatesmocking

Generating unique keys to be used in e.g. an unordered_map for member functions


Just for fun, I'm trying to create a class, that allows "basic mocking functionality". More concretely, you can set return values for function calls. That's it ;D

I have a solution for that, however I don't really like it. I'll show the code, give an explanation and tell what I don't like about it. I am using C++20.

#include <any>
#include <iostream>
#include <string_view>
#include <typeindex>
#include <unordered_map>

using namespace std;

class MockHelper {
public:
    virtual ~MockHelper() = default;

    template<typename Class, typename Ret, typename ...Args>
    void
    setFunctionReturnValue(const string_view &functionName, Ret(Class::*functionPtr)(Args...), const Ret &returnValue) {
        functionsReturnValues[functionName][type_index(typeid(functionPtr))] = make_any<Ret>(returnValue);
    }

protected:
    template<typename Class, typename Ret, typename ...Args>
    Ret handleCall(const string_view &functionName, Ret(Class::*functionPtr)(Args...)) {
        const auto &returnValues = functionsReturnValues[functionName];
        const auto typeIndex = type_index(typeid(functionPtr));
        if (!returnValues.contains(typeIndex))
            return {};
        return any_cast<Ret>(returnValues.at(typeIndex));
    }

private:
    unordered_map<string_view, unordered_map<type_index, any>> functionsReturnValues;
};

class SomethingToMock : public MockHelper {
public:
    virtual int f() {
        return handleCall("f", &SomethingToMock::f);
    }

    virtual int g() {
        // g is overloaded, thus we need to provide the template arguments explicitly
        return handleCall<SomethingToMock, int>("g", &SomethingToMock::g);
    }

    int g(bool) {
        return handleCall<SomethingToMock, int, bool>("g", &SomethingToMock::g);
    }
};

int main() {
    SomethingToMock beingMocked;

    // Setup expected return values
    beingMocked.setFunctionReturnValue("f", &SomethingToMock::f, 0);
    // again, g is overloaded, thus we need to provide the template arguments explicitly
    beingMocked.setFunctionReturnValue<SomethingToMock, int>("g", &SomethingToMock::g, 1);
    beingMocked.setFunctionReturnValue<SomethingToMock, int, bool>("g", &SomethingToMock::g, 2);

    // Show that this is actually working
    std::cout << "Should be 0 and is actually " << beingMocked.f() << std::endl;
    std::cout << "Should be 1 and is actually " << beingMocked.g() << std::endl;
    std::cout << "Should be 2 and is actually " << beingMocked.g(true) << std::endl;

    return 0;
}

class MockHelper allows to set return values for function calls (via setFunctionReturnValue), and allows to execute those function calls from derived classes (via handleCall).

class SomethingToMock derives from class MockHelper and contains 3 member functions which I use to show stuff.

In the main function we setup return values and show that it actually works.

A LIVE DEMO.

I dislike, that I have to give functions names as strings to all relevant functions. This is because if I wouldn't I couldn't distinguish the functions. E.g.

type_index(typeid(&SomethingToMock::f)) == type_index(typeid(static_cast<int (SomethingToMock::*)()>(&SomethingToMock::g)))

evaluates to true, which means f() and g() cannot be distinguished by the signatures only, we have to include the names as well.

So, my question is, can we somehow generate unique keys for all the functions, so that they can be used in e.g. an unordered_map, without having to explicitly give the names of the functions? So, as an example the main function should look more like this:

int main() {
    SomethingToMock beingMocked;

    // Setup expected return values
    beingMocked.setFunctionReturnValue(&SomethingToMock::f, 0);
    // again, g is overloaded, thus we need to provide the template arguments explicitly
    beingMocked.setFunctionReturnValue<SomethingToMock, int>(&SomethingToMock::g, 1);
    beingMocked.setFunctionReturnValue<SomethingToMock, int, bool>(&SomethingToMock::g, 2);

    // Show that this is actually working
    std::cout << "Should be 0 and is actually " << beingMocked.f() << std::endl;
    std::cout << "Should be 1 and is actually " << beingMocked.g() << std::endl;
    std::cout << "Should be 2 and is actually " << beingMocked.g(true) << std::endl;

    return 0;
}

Solution

  • It is possible to generate a unique hashable/comparable value at compile-time for each member function. This answer will generate a unique MethodId value for each member function, defined as:

    struct MethodTag {};
    using MethodId = MethodTag const*; // hashable, comparable
    

    First, let's define some generic traits and helpers:

    // Extracts information from a member function.
    template<typename T>
    struct MethodTraits {
        static constexpr auto isMethod = false;
    };
    
    template<typename RetType_, typename Object_, typename... Args_>
    struct MethodTraits<RetType_ (Object_::*)(Args_...)> {
        static constexpr auto isMethod = true;
    
        using RetType = RetType_; // return type of the member function
        using Object = Object_; // "*this" type of the member function
    };
    
    // Checks if a type represents a member function.
    template<typename T>
    concept cMethod = MethodTraits<T>::isMethod;
    
    // Gets the return type of a member function.
    template<cMethod auto method>
    using ReturnTypeOf = typename MethodTraits<decltype(method)>::RetType;
    

    The unique MethodId of a member function can simply be generated with:

    template<cMethod auto method>
    struct UniqueMethodId {
        static constexpr auto tag = MethodTag{}; // unique tag for `method`.
    };
    
    template<cMethod auto method>
    constexpr MethodId uniqueMethodId() {
        return &UniqueMethodId<method>::tag;
    }
    

    The key point here is that the member function pointer method (ex: &SomethingToMock::f) is passed by value, not just by type. This ensures that different member functions get a different MethodId, even if they have the same arguments/return type.

    The original code can then be refactored as:

    class MockHelper {
    public:
        virtual ~MockHelper() = default;
    
        template<cMethod auto method>
        void setFunctionReturnValue(ReturnTypeOf<method> const& returnValue) {
            using RetType = ReturnTypeOf<method>;
            static constexpr auto methodId = uniqueMethodId<method>();
            functionsReturnValues[methodId] = std::make_any<RetType>(returnValue);
        }
    
    protected:
        template<cMethod auto method>
        auto handleCall() {
            using RetType = ReturnTypeOf<method>;
            static constexpr auto methodId = uniqueMethodId<method>();
            return std::any_cast<RetType>(functionsReturnValues.at(methodId));
        }
    
    private:
        std::unordered_map<MethodId, std::any> functionsReturnValues;
    };
    
    class SomethingToMock : public MockHelper {
    public:
        virtual int f() { // virtual methods are fine.
            return handleCall<&SomethingToMock::f>();
        }
    
        virtual int g() {
            using Method = int (SomethingToMock::*)(); // overload resolution
            return handleCall<Method{&SomethingToMock::g}>();
        }
    
        int g(bool) {
            using Method = int (SomethingToMock::*)(bool); // overload resolution
            return handleCall<Method{&SomethingToMock::g}>();
        }
    };
    
    int main() {
        SomethingToMock beingMocked;
    
        // Setup expected return values
        beingMocked.setFunctionReturnValue<&SomethingToMock::f>(0);
        static constexpr int (SomethingToMock::*gVoid)() = &SomethingToMock::g; // overload resolution
        beingMocked.setFunctionReturnValue<gVoid>(1);
        static constexpr int (SomethingToMock::*gBool)(bool) = &SomethingToMock::g; // overload resolution
        beingMocked.setFunctionReturnValue<gBool>(2);
    
        // Show that this is actually working
        std::cout << "Should be 0 and is actually " << beingMocked.f() << std::endl;
        std::cout << "Should be 1 and is actually " << beingMocked.g() << std::endl;
        std::cout << "Should be 2 and is actually " << beingMocked.g(true) << std::endl;
    
        return 0;
    }
    

    Live demo (compiler explorer)