C++ 使用';可变的';用于常量方法中异步填充的缓存
我担心我正在破坏C++ 使用';可变的';用于常量方法中异步填充的缓存,c++,qt,c++11,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
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()
必须是常量,这就要求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 ];
我担心的是,数据模型对象的逻辑常量在这里被破坏:调用某些索引的data()
,可能会直接导致将来调用相同的参数,返回不同的值
这是个坏主意吗?是否有一个共同的模式/范例来“正确”地完成它
脚注:我对
SignalRowDataUpdate()
也有类似的问题。这实际上是一个发出Qt信号的包装器:emit dataChanged(from,to)
,这是一个非常量调用。我通过在构造时在lambda中捕获this
来处理这个问题,允许我从const函数调用non-const方法。对于C++语言标准中的章节[类],我不感到自豪(< P>),
如果成员函数声明为const,则其类型为const X*
因此,常量
改变了这个
的类型;类的“逻辑常量”没有什么(我发现的)。因为这是指向MyDataModel
的常量指针,你能用它做什么?[decl.type.cv]告诉我们
除了任何声明为可变的类成员都可以修改之外,任何试图在常量对象的生存期(3.8)内修改它的行为都会导致未定义的行为
允许在data()
中对mCache
进行更改,因为它声明为mutable
当使用相同的参数调用常量成员函数时,不要求该函数必须返回相同的值,因为内部状态可能会通过更新可变成员或干预对非常量成员函数的调用而改变
总之,只要调用代码能够处理三个不同的返回值(QVariant::Invalid
,如果当前正在加载数据,则为空的RowData
对象,以及已加载的行),那么成员函数声明就不会有任何问题
当您将新加载的数据移动到缓存中时,回调中可能会出现问题。如果另一个线程同时尝试加载该行的数据,它可能会得到一个RowData
对象,该对象是空行和加载行的混合体
在
SignalRowDataUpdated
中,为什么要通过lambda进行间接寻址,而不是对this
执行const\u cast
?这是对mutable的滥用。显然,您已经破坏了const的契约。对象与客户端不再明显一致
真正的答案是数据(…)根本不是常量。我假设数据(…)必须是常量,因为您覆盖的任何内容都不能更改
要么重新排列一大堆代码以将缓存移出此类,要么在单独的方法中以某种方式更新缓存,要么使用mutable改变规则
不要仅仅为了成为一个纯粹主义者而改变一个正确的、有效的实现。在数据前面抛出一条注释(…),然后完成它
如果这是一个有许多用户(API用户)的大型项目,我会说让负责常量的人将其删除,或者添加另一种方法,例如dataVersion2,它修改缓存而不是常量。首先,今天的常量成员函数与逻辑常量无关。它与线程安全有关(请参阅) 最重要的是,如果将成员函数设置为const,则应该保证如果从多个线程调用该函数,则不会出现争用条件
如果您不喜欢类定义中的缓存,请使用间接寻址,在类定义中创建一个指向缓存的智能指针。您不需要可变,编译器也不会因为函数是常量而感到不安:)MyDataModel::data()的契约可以定义为使函数可以返回值或不返回值,并且不会阻塞。如果它返回
QVariant::Invalid
,则将来必须调用回调
类在第一次查询data()
时启动异步请求这一事实是类内部的一部分,并且data()
不会以从外部可见的方式修改对象:data()
仍然可以或不可以立即为下一次调用返回值
如果MyDataModel
的用户依赖于这样一个事实,即data()
将在缓存该值后立即返回该值,并在上一次使用回调传递该值。要使其在逻辑上保持常量,发生这些行为中的哪一个需要保持未定义状态,即调用方必须始终期望这两种行为
如果函数
data()
总是使用回调传递值,并且从不返回值,那么接口可能会更干净。如果数据在mCache
中,它将立即调用回调。然后,契约将是data()
(可能重命名为async\u retrieve()
或类似名称)导致将来使用请求的值调用回调。它仍然可以是常量
,因为对象不会与外部不同
还有另一个函数sync\u retrieve()
,其中调用者总是通过返回val同步接收值