再一次问好。我继续一点一点地为 Windows 编写我的 GUI 库,同时学习 C++。我在各种资料中读到,从您的库中导出标准 STL 类不是很好,因为将来在具有不同版本 STL 的环境中使用此类库时可能会导致各种困难(如果事实上这不是案例-纠正我)。因此,为了不导出 std:: 函数,我决定编写类似我自己的回调的东西。经过几个小时的谷歌搜索,我们成功地诞生了以下课程:
/**
* \brief Частичное шаблонное объявление
* \detail Обеспечивает возможность указания параметров шаблона в виде "ReturnType(Arguments...)"
* \tparam T Возвращаемый тип функции обратного вызова
*/
template<typename T>
class WQUERY_API Callback;
/**
* \brief Класс-обретка над функцией (либо любым callable объектом) обратного вызова
* \tparam ReturnType Возвращаемый тип функции обратного вызова
* \tparam Arguments Аргументы функции обратного вызова
*/
template <typename ReturnType, typename ... Arguments>
class Callback<ReturnType(Arguments...)>
{
private:
ReturnType(*wrapperFunction_)(Arguments... args, void*); // Указатель на функцию-обретку (вызывает настоящий callable объект)
void *pCallable_; // Указатель на callabl'e объект объект
public:
/**
* \brief Конкструктор по умолчанию
*/
Callback() :pCallable_(nullptr) {}
/**
* \brief Установка функции обратного вызова
* \tparam T Тип callabl'e объекта
* \param pCallable Указатель на функцию (callable объект)
*/
template<class T>
void Set(T pCallable)
{
// Выделить память под функцию (callable объект) и скопировать содежимое в эту память
// если это лямбда, потребуется столько памяти, сколько занимают ее "захваченные" переменные
this->pCallable_ = malloc(sizeof(T));
memcpy(this->pCallable_, &pCallable, sizeof(T));
// Создать функцию "обертку", которая инициирует вызов callable объекта с аргументами args
this->wrapperFunction_ = [](Arguments... args, void *callable) ->ReturnType {
return (*reinterpret_cast<T*>(callable))(args...);
};
}
/**
* \brief Вызов функции обертки
* \param args Аргументы
* \return Значение, возвращаемое оберткой (идентично тому что вернет pCallable_)
*/
ReturnType Invoke(Arguments... args)
{
if (this->IsSet())
{
return this->wrapperFunction_(args..., this->pCallable_);
}
return ReturnType();
}
/**
* \brief Сброс функции (очистить память и сделать не вызываемой)
*/
void Unset()
{
if (this->pCallable_) {
free(this->pCallable_);
this->pCallable_ = nullptr;
}
}
/**
* \brief Установлено ли
* \return Состояние
*/
bool IsSet() const
{
return this->pCallable_ && this->wrapperFunction_;
}
/**
* \brief Деструктор (срабсывает функцию)
*/
~Callback()
{
this->Unset();
}
}
计划以某种方式使用这个东西:
1)在Window类(或控件类)中,用Callback类的一组对象声明一个结构
struct
{
Callback<bool()> onClose;
Callback<void()> onPaint;
Callback<void(unsigned int type, Vector2D<int> newSizes)> onResized;
Callback<void(unsigned int code)> onKeyDown;
Callback<void(unsigned int code)> onKeyUp;
Callback<void(char symbol)> onTyping;
Callback<void(Vector2D<int> cursor)> onMouseMove;
Callback<void(Vector2D<int> cursor, MouseKeys type)> onMouseKeyDown;
Callback<void(Vector2D<int> cursor, MouseKeys type)> onMouseKeyUp;
} events;
2)使用库时,回调将像这样指定
wquery::Window window1;
window1.events.onClose.Set([]() {std::cout << "Test" << std::endl; return true; });
3)在窗口过程中,在正确的地方,函数将被调用(如果它们已安装)
case WM_CLOSE:
if (window)
{
if(window->events.onClose.IsSet())
{
if (!window->events.onClose.Invoke()) return 0;
}
if(window->closesProgram_) PostQuitMessage(0);
}
ShowWindow(hWnd, SW_HIDE);
break;
起初,一切都很顺利,你可以同时给 Set 函数的参数一个 lambda 和一个常规函数指针,它们被完美地调用了。但是对Window析构函数的调用,不幸的是,在执行过程中开始产生如下错误:

原来,这个错误是由Callback类本身的内存释放产生的。而这个特殊的时刻
void Unset()
{
if (this->pCallable_) {
free(this->pCallable_);
this->pCallable_ = nullptr;
}
}
目前,我非常不清楚问题出在哪里,因为我没有删除我无权访问的内存,而是删除了 COPYED 内存(在 Set 函数中,它正在复制)。有趣的是,这个类在库之外没有问题。也许我在这个复制过程中遗漏了一些重要的细节?或者也许你应该以不同的方式来实现你的回调?
这里有几个问题:
在 C++ 中你不应该使用
malloc,如果你这样做了,你应该检查结果是否不是nullptr通过简单的字节复制进行的初始化只能合法地用于简单复制的对象(例如 int、void *)。如果您
T有某种捕获 lambda 作为std::string副本,则该副本将包含指向原始字符串所拥有的缓冲区的潜在无效指针。在 C++ 中,您应该调用复制构造函数。那些。代替称呼
该类
Callback将使用编译器生成的复制构造函数(和赋值运算符),它将简单地复制另一个对象拥有的指针的值。这将导致双重释放错误。您应该编写自己的复制(移动)构造函数(和运算符)的变体。在 C++ 中,您不应该处理手动删除内存 - 这就是智能指针的用途。
最后两点被称为3/5/0 规则。
至于在不同版本的 stl 的环境中使用这样的库 - 不幸的是,二进制兼容性确实存在困难。甚至在由同一个编译器收集的具有不同参数的代码之间。这样的包装器在这方面无济于事。传统上唯一的方法是使用最小的 C api。