Генерация уникальных ключей, которые будут использоваться, например. unordered_map для функций-членов

Просто ради развлечения я пытаюсь создать класс, который обеспечивает «базовую функциональность насмешек». Более конкретно, вы можете установить возвращаемые значения для вызовов функций. Вот и все ;D

У меня есть решение для этой проблемы, но оно мне не очень нравится. Я покажу код, дам пояснения и расскажу, что мне в нем не нравится. Я использую С++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 позволяет устанавливать возвращаемые значения для вызовов функций (с помощью setFunctionReturnValue) и позволяет выполнять эти вызовы функций из производных классов (с помощью handleCall).

class SomethingToMock происходит от class MockHelper и содержит 3 функции-члена, которые я использую для отображения.

В функции main мы устанавливаем возвращаемые значения и показываем, что это действительно работает.

ЖИВАЯ ДЕМО .

Мне не нравится, что мне приходится давать имена функций в виде строк всем соответствующим функциям. Это потому, что если бы я этого не сделал, я бы не смог различить функции. Например.

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

оценивается как true, что означает, что f() и g() нельзя отличить только по сигнатурам, мы также должны включить имена.

Итак, мой вопрос: можем ли мы каким-то образом сгенерировать уникальные ключи для всех функций, чтобы их можно было использовать, например. unordered_map без необходимости явно указывать имена функций? Итак, в качестве примера функция main должна выглядеть примерно так:

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;
}

Почему бы не использовать в качестве ключа адрес функции вместо имени функции? Вы уже передаете это setFunctionReturnValue.

izzy18 15.05.2024 22:11

Я считаю, что значения указателей функций-членов должны быть разными для данного класса. Таким образом, вы можете просто использовать указатель непосредственно в качестве ключа. Задача будет заключаться в хешировании указателя функции-члена. Вероятно, вы могли бы memcpy указатель на целое число достаточного размера или, по крайней мере, на часть указателя, которая соответствует наибольшему целочисленному типу, а затем хешировать его. Хотя это может иметь некоторые проблемы, связанные с представлением целочисленных ловушек на некоторых платформах. Если карта не слишком велика, просто возврат 0 в качестве хеша во всех случаях будет работать легко, но выполните линейный поиск.

François Andrieux 15.05.2024 22:11

Немного погуглив, я не нашел разумного способа генерировать хэш (и оператор ==) для таких указателей на функции-члены. Говоря разумно, я имею в виду, что это действительно будет работать при разных обстоятельствах, то есть при разных компиляторах и т. д.

MockingMocker 15.05.2024 22:57

@MockingMocker operator== уже работает с указателями на функции-члены. Если вы действительно хотите это сделать, вы можете просто принять линейный поиск. Если у вас одновременно нет ошеломляющего количества имитируемых функций, это, скорее всего, нормально.

François Andrieux 16.05.2024 04:36

@FrançoisAndrieux Есть ли у вас ссылка, подтверждающая ваше утверждение о том, что оператор == уже работает с указателями на функции-члены?

MockingMocker 16.05.2024 12:06

@FrançoisAndrieux Вы правы, если у нас нет виртуальных функций-членов, как в моем примере в исходном вопросе. Я не ожидал, что это будет иметь существенное значение, поэтому не стал раздувать этот пример виртуальными функциями-членами. Однако на самом деле функции будут виртуальными, поэтому сравнение использовать нельзя: Если какая-либо из них является указателем на виртуальную функцию-член, результат не указан. Я собираюсь отредактировать свой вопрос и включить эту информацию.

MockingMocker 16.05.2024 14:38

Можем ли мы предположить, что везде, где вызываются setFunctionReturnValue() или handleCall(), имитируемая функция-член известна во время компиляции? Конкретно: разрешено ли в ответе менять API с handleCall(&SomethingToMock::f, ...) на handleCall<&SomethingToMock::f>(...)?

Vincent Saulue-Laborde 16.05.2024 15:38

@VincentSaulue-Laborde Да, это было бы хорошо.

MockingMocker 16.05.2024 15:46
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
9
113
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Во время компиляции для каждой функции-члена можно сгенерировать уникальное хешируемое/сопоставимое значение. Этот ответ сгенерирует уникальное значение MethodId для каждой функции-члена, определенное как:

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

Во-первых, давайте определим некоторые общие свойства и помощники:

// 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;

Уникальный MethodId функции-члена можно просто сгенерировать с помощью:

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;
}

Ключевым моментом здесь является то, что указатель функции-члена method (например: &SomethingToMock::f) передается по значению, а не только по типу. Это гарантирует, что разные функции-члены получат разные MethodId, даже если они имеют одинаковые аргументы/тип возвращаемого значения.

Исходный код затем можно реорганизовать следующим образом:

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;
}

Живая демо-версия (проводник компилятора)

В handleCall и setFunctionReturnValue обязательно, чтобы methodId было static constexpr auto вместо просто auto?

MockingMocker 16.05.2024 18:08

@MockingMocker В этом нет необходимости, просто auto все равно будет правильно. Я не ожидаю, что в этом конкретном случае это будет иметь какое-либо значение для оптимизированной сборки.

Vincent Saulue-Laborde 16.05.2024 18:59

Другие вопросы по теме