13

我正在处理一些 C++ 代码,其中我有几个具有私有方法的管理器对象,例如

void NotifyFooUpdated();

它调用OnFooUpdated()此对象的侦听器上的方法。

请注意,它们不会修改此对象的状态,因此从技术上讲,它们可以成为const方法,即使它们通常会修改整个系统的状态。特别是,侦听器对象可能会回调该对象并对其进行修改。

就我个人而言,我想让它们保持原样而不是声明它们const

但是,我们的静态代码检查器 QAC 将此标记为偏差,因此我必须声明它们const,或者我必须争论为什么它们应该保持非常量并获得偏差许可。

不声明这些方法的论据是什么const
还是我应该遵循 QAC 并声明它们const
我应该采用仅限于此对象的严格局部观点,还是将系统视为一个整体?

4

13 回答 13

3

粗略地说,你有一个容器类:一个满是观察者的经理。在 C 和 C++ 中,您可以拥有具有非常量值的 const 容器。考虑是否删除了一层包装:

list<Observer> someManager;

void NotifyFooUpdated(const list<Observer>& manager) { ... }

您不会看到全局 NotifyFooUpdated 采用 const 列表没有什么奇怪的,因为它不会修改列表。该 const 参数实际上使参数解析更加宽松:该函数同时接受 const 和非 const 列表。类方法版本上的所有 const 注释都意味着const *this.

为了解决另一个观点:

如果您不能保证调用函数的对象在函数调用前后保持不变,则通常应将其保留为非常量。

仅当调用者拥有对该对象的唯一引用时,这才是合理的。如果对象是全局的(就像在原始问题中一样)或在线程环境中,任何给定调用的常量性都不能保证对象的状态在调用过程中保持不变。一个没有副作用并且总是为相同的输入返回相同值的函数是pure。NotifyFooUpdate() 显然不是纯粹的。

于 2010-10-27T18:25:09.427 回答
3

如果侦听器存储为指针集合,即使您的对象是 const,您也可以对它们调用非常量方法。

如果约定是监听器可以在收到通知时更新其状态,那么该方法应该是非常量的。

您是说侦听器可能会回调对象并对其进行修改。但是侦听器不会自行更改 - 因此 Notify 调用可能是 const 但您将指向您自己的对象的非常量指针传递给它。

如果侦听器已经拥有该指针(它只侦听一件事),那么您可以将这两种方法都设为 const,因为您的对象被修改是一种副作用。正在发生的事情是:

A 调用 B B 修改 A 结果。

所以 A 调用 B 间接导致它自己的修改,但不是对自身的直接修改。

如果是这种情况,您的方法都可以并且可能应该是 const。

于 2010-10-19T13:13:21.397 回答
2

不声明这些方法的论据是什么const
还是我应该遵循 QAC 并声明它们const
我应该采用仅限于此对象的严格局部观点,还是将系统视为一个整体?

你知道的是,这个被调用的经理对象不会改变。管理器随后调用函数的对象可能会改变,也可能不会。你不知道。

根据您的描述,我可以设想这样一种设计,其中所有涉及的对象都在const(并且可以通过将通知写入控制台来处理通知)。如果你不做这个函数const,你禁止这个。如果你做到了const,你允许两者。

我想这是一个支持做这个的论据const

于 2010-10-19T13:01:48.707 回答
1

反对const有一些很好的论据,这里是我的看法:-

就个人而言,我不会将这些“OnXXXUpdated”作为我的经理课程的一部分。我认为这就是为什么在最佳实践方面存在一些混淆。您正在通知相关方某事,并且不知道在通知过程中对象的状态是否会发生变化。它可能会,也可能不会。对我来说显而易见的是,通知相关方的过程应该是一个常量。

所以,为了解决这个困境,这就是我要做的:

从管理器类中删除 OnXXXXUpdated 函数。

编写一个通知管理器,这是一个原型,具有以下假设:

“Args”是一个任意基类,用于在通知发生时传递信息

“Delegate”是某种函数指针(例如 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);
};

其他类可以收听这些东西。

我意识到我刚刚输入了观察者模式,但我的意图是表明问题在于这些东西是如何被提出的,以及它们是否处于 const 状态。通过从管理器类中抽象出通知过程,通知的接收者可以自由地修改该管理器类。只是不是通知管理器。我认为这是公平的。

此外,恕我直言,在一个地方发出通知是一个很好的做法,因为它为您提供了一个可以跟踪通知的地方。

于 2010-10-29T13:01:52.003 回答
1

My take on it is that they should remain non const. This is based on my perception that the manager object's state, is actually the aggregate of the states of all the objects it manages plus any intrinsic state i.e., State(Manager) = State(Listener0) + State(Listener1) + ... + State(ListenerN) + IntrinsicState(Manager).

While encapsulation in the source code may belie this run-time relationship. Based on your description, I believe this aggregated state is reflective of the run-time behavior of the program.

To reinforce my argument: I assert that the code should strive to reflect the programs run-time behavior in preference to strict adherence to the exact semantics of compilation.

于 2010-10-28T18:20:00.433 回答
1

const,还是不要 const:这是个问题。

参数const

  • 有问题的方法不会修改对象的状态。
  • 您的静态代码检查器将缺少 const 标记为偏差,也许您应该听听它。

反对的论点const

  • 这些方法作为一个整体修改系统的状态。
  • 侦听器对象我修改对象。

就我个人而言,我会将其保留为const,它可能会修改整个系统的状态这一事实非常类似于空指针引用。这是一种const方法,它不会修改有问题的对象,但它会使您的程序崩溃,从而修改整个系统的状态。

于 2010-10-28T23:23:01.803 回答
1

如果您不能保证调用函数的对象在函数调用前后保持不变,则通常应将其保留为非常量。想想看——你可以写一个监听器,在对象是非常量的时候插入它,然后使用这个函数来违反 const 的正确性,因为你曾经在过去是非常量的时候访问过那个对象。那是错误的。

于 2010-10-23T11:06:46.770 回答
0

const 正确性具有(有意)传播方式。您应该在任何可以摆脱它的地方使用 const,而 const_cast 和 c-style-casts 应该是处理客户端代码的工件 - 从不在您的代码中,但非常罕见的例外。

如果on时void NotifyFooUpdated();调用不是 const,那么您应该明确限定此突变。如果您的代码在整个过程中都是 const 正确的(我正在质疑),那么明确(通过方法声明/侦听器访问)您正在改变侦听器(成员),那么应该符合非常量。因此,您只需将突变声明为尽可能接近源,它应该检查并且 const-correctness 将正确传播。listeners[all].OnFooUpdated()OnFooUpdated()NotifyFooUpdated()

于 2010-10-25T21:27:37.303 回答
0

const 表示对象的状态不被成员函数修改,不多也不少。它与副作用无关。因此,如果我正确理解您的情况,对象的状态不会改变,这意味着函数必须声明为 const,应用程序其他部分的状态与该对象无关。即使有时对象状态有非 const 子对象,它们不是对象逻辑状态的一部分(例如互斥体),函数仍然必须设为 const,并且这些部分必须声明为可变的。

于 2010-10-29T20:45:36.977 回答
0

将虚函数设为 const 始终是一个艰难的决定。使它们成为非常量是最简单的方法。在许多情况下,侦听器函数应该是 const:如果它不改变侦听方面(对于此对象)。如果监听一个事件会导致监听方注销自己(一般情况下),那么这个函数应该是非常量的。

虽然对象的内部状态可能会在调用 OnFooChanged 时发生变化,但在接口级别,下次调用 OnFooChanged 时,将产生类似的结果。这使它成为常量。

于 2010-10-26T10:50:03.393 回答
0

在您的类中使用 const 时,您正在帮助该类的用户了解该类将如何与数据交互。你在签订合同。当您引用一个 const 对象时,您就知道对该对象进行的任何调用都不会改变其状态。该引用的常量仍然只是与调用者的合同。该对象仍然可以使用可变变量在后台自由地执行一些非常量操作。这在缓存信息时特别有用。

例如,您可以使用以下方法上课:

int expensiveOperation() const
{
    if (!mPerformedFetch)
    {
        mValueCache = fetchExpensiveValue();
        mPerformedFetch = true;
    }
    return mValueCache;
}

此方法第一次执行可能需要很长时间,但会缓存结果以供后续调用。您只需要确保头文件将变量 performFetch 和 valueCache 声明为可变。

class X
{
public:
    int expensiveOperation() const;
private:
    int fetchExpensiveValue() const;

    mutable bool mPerformedFetch;
    mutable int mValueCache;
};

这让一个 const 对象与调用者建立合约,并在后台工作时表现得像 const 一样。

我建议让您的班级将侦听器列表声明为可变的,并使其他所有内容尽可能为 const。就对象的调用者而言,对象仍然是 const 并以这种方式运行。

于 2010-10-26T21:09:02.717 回答
0

我猜你正在关注 HICPP 或类似的东西。

我们要做的是,如果我们的代码违反了 QACPP,并且我们认为它是错误的,那么我们会通过 Doxygen 记录它(通过 addtogroup 命令,这样您就可以轻松获得它们的列表),给出一个理由来证明我们违反它的原因,然后禁用警告通过//PRQA命令。

于 2010-10-19T12:46:21.977 回答
0

请注意,它们不会修改此对象的状态,因此从技术上讲,它们可以成为 const 方法,即使它们通常会修改整个系统的状态。特别是,侦听器对象可能会回调该对象并对其进行修改。

既然监听器可以改变一个状态,那么这个方法不应该是 const。从您写的内容来看,听起来您正在使用大量 const_cast 并通过指针调用。

于 2010-10-25T20:58:44.597 回答