我正在研究一些C ++代码,其中我有几个带有私有方法的管理器对象,例如
void NotifyFooUpdated();
在此对象的侦听器上调用OnFooUpdated()
方法。
请注意,它们不会修改此对象的状态,因此它们在技术上可以成为const
方法,即使它们通常会修改整个系统的状态。特别是,侦听器对象可能会回调此对象并对其进行修改。
就我个人而言,我希望保留它们,而不是声明它们const
。
但是,我们的静态代码检查器QAC将此标记为偏差,因此我要么必须声明它们const
,要么我必须争论为什么它们应该保持非常量并获得偏差的补助。 / p>
没有声明这些方法const
的论据是什么?
或者我应该关注QAC并声明它们const
?
我应该采用仅限于此对象的严格本地观点,还是将系统视为一个整体?
答案 0 :(得分:3)
如果侦听器存储为指针集合,即使对象是const,也可以对它们调用非const方法。
如果合同是侦听器在收到通知时可能更新其状态,则该方法应为非const。
你是说听众可以回调对象并修改它。但是监听器不会自行改变 - 因此Notify调用可以是const,但是你将一个非const指针传递给你自己的对象。
如果侦听器已经有了该指针(它只侦听一个东西),那么你可以将两个方法都设为const,因为你的对象被修改是一个副作用。发生的事情是:
A来电B B修改A作为结果。
因此,呼叫B间接导致其自身的修改,但不是对自我的直接修改。
如果是这种情况,你的方法可能也可能应该是const。
答案 1 :(得分:3)
松散地说,你有一个容器类:一个充满观察员的经理。在C和C ++中,您可以使用非const值的const容器。考虑一下你是否删除了一层包装:
list<Observer> someManager;
void NotifyFooUpdated(const list<Observer>& manager) { ... }
对于采用const列表的全局NotifyFooUpdated,您会发现没什么奇怪的,因为它不会修改列表。该const参数实际上使得参数解析更加宽松:该函数接受const和非const列表。类方法版本的所有const注释都是const *this
。
解决另一个问题:
如果你不能保证调用函数的对象在函数调用之前和之后保持不变,你通常应该把它保留为非const。
如果调用者只有对象的引用,那是唯一合理的。如果对象是全局的(就像在原始问题中那样)或在线程环境中,则任何给定调用的常量都不能保证对象的状态在调用期间保持不变。没有副作用且始终为相同输入返回相同值的函数是 pure 。 NotifyFooUpdate()显然不是纯粹的。
答案 2 :(得分:2)
没有声明这些方法
const
的论据是什么? 或者我应该关注QAC并声明它们const
?
我应该采用仅限于此对象的严格本地观点,还是将系统视为一个整体?
你知道的是那个管理员对象需要做而不是更改。然后,管理器在上调用函数的对象可能会更改,或者它们可能不会。你不知道。
根据您的描述,我可以设想这样一种设计,其中所有涉及的对象都是const
(并且可以通过将它们写入控制台来处理通知)。如果您不执行此功能const
,则禁止此操作。如果你成功const
,你允许两者。
我想这是支持制作const
的论据。
答案 3 :(得分:1)
如果无法保证调用函数的对象在函数调用之前和之后保持不变,则通常应将其保留为非const。想一想 - 你可以编写一个监听器,在对象是非const时插入它,然后使用这个函数来违反const的正确性,因为你曾经有过访问过去那个非const的对象。那是错的。
答案 4 :(得分:1)
我认为它们应该保持非常量。这是基于我的看法,即管理者对象的状态,实际上是它管理的所有对象的状态的总和加上任何内在状态,即State(Manager) = State(Listener0) + State(Listener1) + ... + State(ListenerN) + IntrinsicState(Manager)
。
虽然源代码中的封装可能与这种运行时关系相悖。根据您的描述,我相信这个聚合状态反映了程序的运行时行为。
强化我的论点:我断言代码应该努力反映程序的运行时行为,而不是严格遵守编译的确切语义。
答案 5 :(得分:1)
const
,或不 const
:这就是问题。
const
的参数:
反对const
:
就我个人而言,我将它留作const
,事实上它可能会修改整个系统的状态,就像一个空指针引用。它是一个const
方法,它不会修改有问题的对象,但它会使程序崩溃,从而修改整个系统的状态。
答案 6 :(得分:1)
对于 const 有一些很好的论据,所以这里是我的看法: -
就个人而言,我不会将这些“OnXXXUpdated”作为我的经理课程的一部分。我认为这就是为什么对最佳实践存在一些困惑。您正在向相关方通知某些事情,并且不知道在通知过程中对象的状态是否会发生变化。它可能会,也可能不会。 对我来说显而易见的是,通知感兴趣方的过程应为const。
所以,要解决这个困境,我会这样做:
从经理类中删除OnXXXXUpdated函数。
编写一个通知管理器,这是一个原型,具有以下假设:
“Args”是在发生通知时传递信息的任意基类
“委托”是某种函数指针(例如FastDelegate)。
class Args
{
};
class NotificationManager
{
private:
class NotifyEntry
{
private:
std::list<Delegate> m_Delegates;
public:
NotifyEntry(){};
void raise(const Args& _args) const
{
for(std::list<Delegate>::const_iterator cit(m_Delegates.begin());
cit != m_Delegates.end();
++cit)
(*cit)(_args);
};
NotifyEntry& operator += (Delegate _delegate) {m_Delegates.push_back(_delegate); return(*this); };
}; // eo class NotifyEntry
std::map<std::string, NotifyEntry*> m_Entries;
public:
// ctor, dtor, etc....
// methods
void register(const std::string& _name); // register a notification ...
void unRegister(const std::string& _name); // unregister it ...
// Notify interested parties
void notify(const std::string& _name, const Args& _args) const
{
std::map<std::string, NotifyEntry*>::const_iterator cit = m_Entries.find(_name);
if(cit != m_Entries.end())
cit.second->raise(_args);
}; // eo notify
// Tell the manager we're interested in an event
void listenFor(const std::string& _name, Delegate _delegate)
{
std::map<std::string, NotifyEntry*>::const_iterator cit = m_Entries.find(_name);
if(cit != m_Entries.end())
(*cit.second) += _delegate;
}; // eo listenFor
}; // eo class NotifyManager
我已经遗漏了一些代码,但你明白了。我想这个通知管理器将是一个单身人士。现在,确保尽早创建通知管理器,其他管理员只需在构造函数中注册他们的通知,如下所示:
MyManager::MyManager()
{
NotificationMananger.getSingleton().register("OnABCUpdated");
NotificationMananger.getSingleton().register("OnXYZUpdated");
};
AnotherManager::AnotherManager()
{
NotificationManager.getSingleton().register("TheFoxIsInTheHenHouse");
};
现在,当您的经理需要通知相关方时,它只需要调用notify:
MyManager::someFunction()
{
CustomArgs args; // custom arguments derived from Args
NotificationManager::getSingleton().notify("OnABCUpdated", args);
};
其他课程可以听取这些内容。
我意识到我刚刚输入了Observer模式,但我的目的是表明问题在于如何提升这些东西以及它们是否处于const状态。通过从mananager类中抽象出通知的过程,通知的接收者可以自由地修改该经理类。只是没有通知经理。我认为这是公平的。
此外,只有一个地方可以提出通知,这是一个很好的实践,因为它为您提供了一个可以追踪通知的地方。
答案 7 :(得分:0)
我猜您正在关注HICPP或类似的东西。
我们做的是如果我们的代码违反了QACPP并且我们认为它是错误的,那么我们通过Doxygen注意它(通过addtogroup命令以便您轻松获得它们的列表),给出一个理由说明为什么我们违反它然后通过//PRQA
命令禁用警告。
答案 8 :(得分:0)
请注意,它们不会修改状态 这个对象,所以他们可以 技术上是const方法, 即使他们通常修改 整个系统的状态。在 特别是,侦听器对象可能 回调这个对象并修改 它
由于侦听器可以更改状态,因此该方法不应该是const。根据你所写的内容,听起来你正在使用大量的const_cast并通过指针调用。
答案 9 :(得分:0)
const正确性具有(有意的)传播方式。你应该在任何可以使用它的地方使用const,而const_cast和c-style-casts应该是处理客户端代码的工件 - 永远不会在你的代码中,但非常罕见的例外。
如果void NotifyFooUpdated();
调用listeners[all].OnFooUpdated()
而OnFooUpdated()
上的NotifyFooUpdated()
不是常量,那么您应该明确限定此突变。如果你的代码始终是const正确的(我正在质疑),那么明确(通过方法声明/监听器访问)你正在改变监听器(成员),然后{{1}}应该有资格作为非const。所以,你只需要将变异声明为尽可能接近源,它应该检出并且const-correctness将正确传播。
答案 10 :(得分:0)
使虚拟函数const始终是一个困难的决定。使它们成为非常量是一种简单的方法。在许多情况下,侦听器函数应该是const:如果它不改变侦听方面(对于此对象)。 如果侦听事件会导致侦听方取消注册(作为一般情况),则此函数应为非const。
虽然对象的内部状态可能会在OnFooChanged调用上发生更改,但在接口级别,下次调用OnFooChanged时,会产生类似的结果。这使它成为常数。
答案 11 :(得分:0)
在类中使用const时,您可以帮助该类的用户了解类与数据的交互方式。你正在订立合同。当您引用const对象时,您知道对该对象进行的任何调用都不会更改其状态。该引用的常量仍然只是与调用者的契约。该对象仍然可以使用可变变量在后台自由地执行一些非const动作。这在缓存信息时特别有用。
例如,您可以使用方法:
int expensiveOperation() const
{
if (!mPerformedFetch)
{
mValueCache = fetchExpensiveValue();
mPerformedFetch = true;
}
return mValueCache;
}
此方法可能需要很长时间才能执行第一次,但会将结果缓存到后续调用。您只需确保头文件将变量completedFetch和valueCache声明为可变。
class X
{
public:
int expensiveOperation() const;
private:
int fetchExpensiveValue() const;
mutable bool mPerformedFetch;
mutable int mValueCache;
};
这允许const对象与调用者签订合同,并且在后台运行时更聪明一些,就像是const一样。
我建议让你的类将侦听器列表声明为可变,并使其他所有内容尽可能为const。就对象的调用者而言,该对象仍然是const并以此方式起作用。
答案 12 :(得分:0)
Const意味着对象的状态不会被成员函数修改,不多也不少。它与副作用无关。因此,如果我正确理解您的情况,则对象的状态不会更改,这意味着该函数必须声明为const,应用程序的其他部分的状态与此对象无关。即使有时对象状态具有非const子对象(它们不是对象逻辑状态的一部分)(例如互斥体),仍然必须将函数设为const,并且必须将这些部分声明为可变。