Просто ради развлечения я пытаюсь создать класс, который обеспечивает «базовую функциональность насмешек». Более конкретно, вы можете установить возвращаемые значения для вызовов функций. Вот и все ;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;
}
Я считаю, что значения указателей функций-членов должны быть разными для данного класса. Таким образом, вы можете просто использовать указатель непосредственно в качестве ключа. Задача будет заключаться в хешировании указателя функции-члена. Вероятно, вы могли бы memcpy указатель на целое число достаточного размера или, по крайней мере, на часть указателя, которая соответствует наибольшему целочисленному типу, а затем хешировать его. Хотя это может иметь некоторые проблемы, связанные с представлением целочисленных ловушек на некоторых платформах. Если карта не слишком велика, просто возврат 0 в качестве хеша во всех случаях будет работать легко, но выполните линейный поиск.
Немного погуглив, я не нашел разумного способа генерировать хэш (и оператор ==) для таких указателей на функции-члены. Говоря разумно, я имею в виду, что это действительно будет работать при разных обстоятельствах, то есть при разных компиляторах и т. д.
@MockingMocker operator== уже работает с указателями на функции-члены. Если вы действительно хотите это сделать, вы можете просто принять линейный поиск. Если у вас одновременно нет ошеломляющего количества имитируемых функций, это, скорее всего, нормально.
@FrançoisAndrieux Есть ли у вас ссылка, подтверждающая ваше утверждение о том, что оператор == уже работает с указателями на функции-члены?
@FrançoisAndrieux Вы правы, если у нас нет виртуальных функций-членов, как в моем примере в исходном вопросе. Я не ожидал, что это будет иметь существенное значение, поэтому не стал раздувать этот пример виртуальными функциями-членами. Однако на самом деле функции будут виртуальными, поэтому сравнение использовать нельзя: Если какая-либо из них является указателем на виртуальную функцию-член, результат не указан. Я собираюсь отредактировать свой вопрос и включить эту информацию.
Можем ли мы предположить, что везде, где вызываются setFunctionReturnValue() или handleCall(), имитируемая функция-член известна во время компиляции? Конкретно: разрешено ли в ответе менять API с handleCall(&SomethingToMock::f, ...) на handleCall<&SomethingToMock::f>(...)?
@VincentSaulue-Laborde Да, это было бы хорошо.





Во время компиляции для каждой функции-члена можно сгенерировать уникальное хешируемое/сопоставимое значение. Этот ответ сгенерирует уникальное значение 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 В этом нет необходимости, просто auto все равно будет правильно. Я не ожидаю, что в этом конкретном случае это будет иметь какое-либо значение для оптимизированной сборки.
Почему бы не использовать в качестве ключа адрес функции вместо имени функции? Вы уже передаете это
setFunctionReturnValue.