创建一个注册回调

时间:2016-10-18 15:45:20

标签: c++ callback

我希望有一个基类,其目的是将其注册为回调(并在析构函数中注销),回调是一个纯虚函数。像这样。

struct autoregister {
  autoregister() { callback_manager.register(this); }
  ~autoregister() { callback_manager.deregister(this); }
  virtual void call_me()=0;
};

但这对我来说似乎不可靠,我怀疑那里有几种竞争条件。 1)当callback_manager看到指针时,call_me仍然是不可调用的,并且它可能需要任意长的时间直到对象完成构造,2)在调用deregister时,调用派生的对象析构函数,所以回调应该不被称为。

我在思考的一件事就是在callback_manager中检查指针的call_me是否有效,但我找不到一个标准的兼容方式来获取call_me的地址或任何东西。我正在考虑将typeid(指针)与typeid(autoregister *)进行比较,但中间可能有一个抽象类,使得这个不可靠,派生:public middle {};中:public autoregister {} ;,中间的构造函数可以花一个小时,例如加载SQL或搜索谷歌,回调看到它不是基类,并认为可以调用回调,并繁荣。可以这样做吗?

Q1:还有其他竞争条件吗?

Q2:如何正确执行此操作(没有竞争条件,未定义的行为和其他错误)而不要求派生类手动调用寄存器?

问题3:如何检查是否可以在指针上调用虚函数?

4 个答案:

答案 0 :(得分:2)

您应该将回调和注册句柄分开。 callback_manager旁边没有人需要call_me方法,为什么要从外面看到它?使用std::function作为回调,因为它非常方便:任何可调用的都可以转换为它,lambdas非常方便。从回调注册方法返回Handle对象。 Handle将拥有的唯一方法是析构函数,您将从中删除回调。

class Handle {
public:
    explicit Handle(std::function<void()> deleter)
        : deleter_(std::move(deleter))
    {}

    ~Handle()
    {
        deleter_();
    }
private:
    std::function<void()> deleter_;
};

class Manager {
public:
    typedef std::function<void()> Callback;

    Handle subscribe(Callback callback) {
        // NOTE: use mutex here if this method is accessed from multiple threads
        callbacks_.push_back(std::move(callback));
        auto itr = callbacks_.end() - 1;
        // NOTE If Handle lifetime can exceed Manager lifetime, store handlers_ in std::shared_ptr and capture a std::weak_ptr in lambda.
        return Handle([this, itr]{
            // NOTE: use mutex here if this method is accessed from multiple threads
            callbacks_.erase(itr);
        });
    }

private:
    std::list<Callback> callbacks_;
};
  

Q1:还有其他竞争条件吗?

回调/句柄可能会比callback_manager更长,并会尝试从已删除的对象中取消订阅。这可以通过策略(在删除管理器之前始终取消订阅所有内容)或使用弱指针来修复。

如果从多个线程访问callback_manager,您需要使用互斥锁来保护回调存储,那么就会出现明显的竞争。

  

Q2:如何做到这一点(没有竞争条件,未定义的行为   和其他错误)而不要求派生类调用寄存器   手动?

见上文。

  

问题3:如何检查是否可以在指针上调用虚函数?

这是不可能的。

答案 1 :(得分:1)

在autoregister的构造函数中,由于尚未完全构造对象,因此传递给callback_manager的'this'指针很危险。我建议采用略有不同的设计。

struct callback {
  virtual void call_me() = 0;
}

struct autoregister {
  callback*const callback_;

  autoregister(callback*const _callback)
    : callback_(_callback) {
    callback_manager.register(callback_);
  }

  ~autoregister() {
    callback_manager.deregister(callback_);
  }
};

Q1:还有其他竞争条件吗?

也许。如果多个线程可以使用它,则必须同步callback_manager。但我的autoregister版本本身没有竞争条件。

Q2:如何正确执行此操作(没有竞争条件,未定义的行为和其他错误)而不要求派生类手动调用寄存器?

我的代码是我认为可以做到的。

问题3:如何检查是否可以在指针上调用虚函数?

我的代码中没有必要。但一般来说,你可以在类中保留一个标志,在初始化列表中设置为false,并在准备好调用时设置为true。

答案 2 :(得分:1)

竞争条件是当两个线程同时尝试执行某些操作时,结果取决于精确的时间。我不认为此代码段中存在竞争条件,因为this只能由执行构造函数的线程访问。然而callback_manager可能存在竞争条件,但您尚未发布代码,因此我无法分辨。

这里还有另一个问题:对象是从基础构造到最大的,所以在autoregister的构造函数运行时,无法调用虚拟call_me。见this FAQ entry。除了确保完全构建类之外,无法检查虚函数调用是否有效。

通过继承工作的此问题的任何解决方案都无法确保在注册回调之前完全构造正在注册的类,因此必须在注册的类外部进行注册。您可以做的最好的事情是有一些RAII包装器,它在构造时注册对象并在销毁时取消注册,并且可能强制通过处理注册的工厂创建对象。

答案 3 :(得分:1)

我认为@Donghui Zhang走在正确的轨道上,还没有真正到那里,可以这么说。不幸的是,他所做的事情引入了一系列陷阱 - 例如,如果你将本地对象的地址传递给autoregister的ctor,你仍然可以注册一个回调对象,立即超出范围(但不一定立即取消注册)。

我还认为使用call_me作为回调时调用的成员函数来定义回调接口是有问题的(充其量)。如果需要定义可以像函数一样调用的类型,C ++已经为该函数定义了一个名称:operator()。我会强制执行此操作,而不是call_me在场。

要做到这一切,我认为你真的想使用模板而不是继承:

template <class T>
class autoregister {
    T t;
public:
    template <class...Args>
    autoregister(Args && ... args) : t(std::forward(args)...) {
       static_assert(std::is_callable<T>::value, "Error: callback must be callable");

       callback_manager.register(t);
    }

    ~autoregister() { callback_manager.deregister(t); }
};

您可以使用以下内容:

class f {
public:
     virtual void operator()() { /* ... */ }
};

autoregister<f> a;

static_assert确保您作为模板参数传递的类型可以像函数一样被调用。

这也支持通过autoregister将参数传递给它包含的对象的构造函数,所以你可能有类似的东西:

class F {
public:
    F(int a, int b) { ... }
    void operator()() {}
};

autoregister<F> f(1,2);

... 1, 2autoregister构建时将从F传递到int r = callback(1);。另请注意,这并不会尝试为回调函数强制执行特定签名。例如,如果您修改回调管理器以回调为int,那么只有在您注册的回调对象可以使用int参数调用时,代码才会编译,并返回int(或者可以隐式转换为@Override public List<EnterExitDeviceGeofenceResult> reportGeofenceEvent(String locationId, LatLng latLng, boolean enter) throws IOException { String enterExitUrlPart = enter ? "/enter" : "/exit"; String url = getUrl("/users/me/geofences/" + locationId + enterExitUrlPart + "?fields=command{resultingAcState,failureReason,status},deviceId", 2); String body = generateLatLngRequestBody(latLng); return executeCall( deserializers.enterExitGeofenceResult(), new Request.Builder() .url(url) .post(RequestBody.create(JSON, body)) .build(), getClientBuilder() .addInterceptor(defaultErrorInterceptor) .connectTimeout(REPORT_GEOFENCE_EVENT_TIMEOUT, TimeUnit.MILLISECONDS) .readTimeout(REPORT_GEOFENCE_EVENT_TIMEOUT, TimeUnit.MILLISECONDS) .writeTimeout(REPORT_GEOFENCE_EVENT_TIMEOUT, TimeUnit.MILLISECONDS) .build() ); } @Nullable private <T> T executeCall(JsonDeserializer<T> deserializer, Request request, OkHttpClient client) throws IOException { if (deserializer == null) { throw new NullPointerException("deserializer == null"); } StringReader sr = new StringReader(executeCall(request, client)); try { return deserializer.deserialize(new JsonForwardReaderFromJsonReader(new JsonReader(sr))); } catch(Throwable t) { if (logsEnabled) { LogUtil.e(TAG, "Error deserializing", t); } throw t; } } private String executeCall(Request request, OkHttpClient client) throws IOException { ResponseBody responseBody = null; try { if (logsEnabled) { LogUtil.i(TAG, request.url().toString()); } responseBody = client.newCall(request).execute().body(); String responseString = responseBody.string(); if (logsEnabled) { LogUtil.i(TAG, String.format("<- %s", responseString)); } return responseString; } finally { if (responseBody != null) { responseBody.close(); } } } @Override protected void onHandleIntent(Intent intent) { /*some code*/ restApiClient.reportGeofenceEvent(...); } 的东西)。编译器将强制执行具有与其调用方式兼容的签名的回调。这里唯一的缺点是,如果你传递一个可以被调用的类型,但是(例如)不能用回调管理器试图传递的参数调用,那么错误消息就是你get可能不像你想的那样可读。