在const方法中使用'mutable'作为异步填充的缓存

时间:2016-11-25 01:56:16

标签: c++ qt c++11

我担心我违反了mutable的合同,我将其用于在异步执行按需请求的数据模型中缓存信息。数据模型恰好是Qt,尽管这不是一个特别重要的事实。

class MyDataModel : public QAbstractItemModel
{
public:
    QVariant data( const QModelIndex & index, int role ) const override;

private:
    void SignalRowDataUpdated( int row ) const;
    mutable SimpleRowCache mCache;
};

调用data()时,我会检查缓存以查看是否有此缓存。如果没有,我立即返回空数据(以避免阻止UI),并向API发送异步请求以填充缓存。由于data()必须是const,因此mCache是可变的。 data()的内容看起来像这样:

RowData row_data = mCache.Get( row );
if( !row_data )
{
    // Store empty data in cache, to avoid repeated API requests
    mCache.Set( row, RowData() );

    // Invoke API with a lambda to deliver async result.  Note: 'this' is const
    auto data_callback = [this, row]( RowData data )
    {
        mCache.Set( row, std::move(data) );
        SignalRowDataUpdated( row );
    };
    DataApi::GetRowData( row, data_callback );

    return QVariant::Invalid;
}
return row_data[ column ];

我担心的是这里违反了数据模型对象的逻辑const :为某个索引调用data()可以直接导致将来调用具有返回不同值的相同参数。

这是一个坏主意吗?是否有“正确”做到这一点的共同模式/范式?

脚注:我与SignalRowDataUpdated()有类似的问题。这实际上是发出Qt信号的包装器:emit dataChanged( from, to ),这是一个非const调用。我通过在构造时捕获lambda中的this来处理那个,允许我从const函数调用非const方法。我不为此感到骄傲=(

3 个答案:

答案 0 :(得分:3)

首先,今天的const成员函数与逻辑const 无关。 关于线程安全(参见https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/)。

主要的是,如果你创建成员函数 const ,你应该保证如果从多个线程调用它,就不会有竞争条件。

如果你不喜欢你的类定义中的 mutable 缓存,请使用间接,在你的类定义中创建一个指向缓存的智能指针。您不需要 mutable ,编译器不会对您的函数 const 感到不安:)

答案 1 :(得分:0)

可以定义MyDataModel::data()的合同,以便函数可以返回值,并且不会阻止。如果它返回QVariant::Invalid,它必须在将来调用回调。

第一次查询data()时类启动异步请求的事实是类内部的一部分,data()不会以外部可见的方式修改对象:{ {1}}仍然可能会或可能不会立即为下次通话返回值。

问题是,如果data()的用户依赖于MyDataModel返回值immediatley缓存后的事实,并使用回调传递它以前的时间。因为它在逻辑上是const,所以这些行为中的哪一个需要保持未定义,即调用者必须始终期望两者。

如果函数data()总是使用回调传递值,并且永远不返回值,则接口可能更干净。如果数据在data()中,它将立即调用回调。然后合同将是mCache(可能重命名为data()或类似)导致将来使用请求的值调用回调。它仍然可以是async_retrieve(),因为对象不会与外部不同。

也许是另一个函数const,其中调用者总是通过返回值同步接收值,但如果该值尚未缓存,则允许阻塞。

通过此方式,调用者可以决定如何接收值,而sync_retrieve()可以在内部进行缓存以提高效率。

答案 2 :(得分:0)

看起来真正的问题是您的数据(...)方法可能不是线程安全的,除非您确定只能从一个线程调用数据(...)(UI线程可能?我不熟悉Qt。)

假设有两个线程同时调用数据(...):

1)两个线程都可能执行if(!row_data)中的代码,你需要添加一个锁来同步这个并仔细检查mCache.Get(row)以查看它是否'如果(!row_data)

,则为null

2)之后,第一次调用数据(...)引起的SignalRowDataUpdated调用也可能在第二次调用QVariant返回的数据(...)之前执行::无效。我想这不会是你想要的。在这种情况下,您还需要同步数据(...)和SignalRowDataUpdated。