统一处理具有协变类型的函数指针(如何使用派生类型调用回调?

Handling function pointer with covariant types uniformly (how to call callbacks with derived types?)

本文关键字:类型 何使用 派生 回调 调用 指针 处理 函数      更新时间:2023-10-16

假设我有一个Task类:

class Task;
using FinishedCallback = void (Task &);
class Task {
    public:
        // this function is implemented by derived classes
        // there are other virtual functions too
        virtual bool isFinished() = 0;
    private:
        std::vector<FinishedCallback> m_callbacks;
    protected:
        // these functions are called by derived classes
        void addFinishedCallback(FinishedCallback callback) {
            m_callbacks.push_back(callback);
        }
        void callFinishedCallbacks();
};

此类是基类。我想尽可能多地实现它。它有一个m_callbacks向量,它存储任务完成时需要调用的回调(callFinishedCallbacks()这样做(。

现在,从这个类派生:

class MyTask;
using MyFinishedCallback = void (MyTask &);
class MyTask: public Task {
    public:
        void addFinishedCallback(MyFinishedCallback callback) {
            Task::addFinishedCallback(reinterpret_cast<FinishedCallback &>(callback));
        }
        // when MyTask finishes, it calls callFinishedCallbacks
};

在这里,完成的回调具有MyTask &参数,因此我必须reinterpret_cast它。所以我的程序有 UB。(MyTask &类型的原因是我不必在回调中MyTask &Task & - 这是为了方便起见(。

但是,即使我的程序有 UB,它也可以工作(如果MyTask有多个继承,我可能遇到的唯一真正问题(。是否可以修改此程序以删除 UB,同时保留其主要属性:

  • (编译(代码很小
  • 需要少量编码工作,如果一个来自任务
  • 回调不需要包装器

一个想法是对你编写回调的方式进行轻微的改变。

  1. 使用 Task * 而不是 Task & 作为回调参数类型。
  2. 在回调开始时,使用 dynamic_castTask *转换为指向派生类型 ( MyType * ( 的指针。

    void MyCallback(Task *task) {
      auto *mytask = dynamic_cast<MyTask *>(task);
      assert(mytask != nullptr);
      // use mytask from here on
    }
    

另一个想法是使用奇怪的重复模板模式(CRTP(。

class BasicTask {
    public:
        virtual ~BasicTask() = default;
        virtual bool isFinished() = 0;
};
template <typename Self>
class Task : public BasicTask {
    private:
        typedef void FinishedCallback(Self &task);
        std::vector<FinishedCallback> m_callbacks;
    protected:
        void addFinishedCallback(FinishedCallback callback) {
            m_callbacks.push_back(callback);
        }
        void callFinishedCallbacks();
};

然后,您将从任务中派生特定的任务类型,如下所示:

class MyTask : public Task<MyTask> { ... };

这减少了您必须编写的代码量(因为只有一个源代码实现addFinishedCallback(,但它冒着生成更多目标代码的风险。 优化器和链接器可能能够控制这种可能性。