禁止创建临时对象

时间:2009-05-27 09:44:31

标签: c++ mfc temporary

在多线程应用程序中调试崩溃时,我终于在这个语句中找到了问题:

CSingleLock(&m_criticalSection, TRUE);

请注意,它正在创建一个CSingleLock类的未命名对象,因此在此语句之后,临界区对象会立即解锁。这显然不是编码员想要的。此错误是由简单的输入错误引起的。我的问题是,是否有一些我可以防止在编译时自己创建类的临时对象,即上述类型的代码应该生成编译器错误。一般来说,我认为每当一个类尝试进行某种资源获取时,就不应该允许该类的临时对象。有没有办法强制执行呢?

8 个答案:

答案 0 :(得分:13)

编辑:正如j_random_hacker所说,可以强制用户声明一个命名对象以取出锁定。

然而,即使你的班级以某种方式禁止临时创作,用户也可能犯同样的错误:

// take out a lock:
if (m_multiThreaded)
{
    CSingleLock c(&m_criticalSection, TRUE);
}

// do other stuff, assuming lock is held

最终,用户必须了解他们编写的一行代码的影响。在这种情况下,他们必须知道他们正在创建一个对象,他们必须知道它持续多长时间。

另一个可能的错误:

 CSingleLock *c = new CSingleLock(&m_criticalSection, TRUE);

 // do other stuff, don't call delete on c...

哪会导致你问“我有什么方法可以阻止我的班级用户在堆上分配它”?答案是一样的。

在C ++ 0x中,通过使用lambdas,将有另一种方法来完成所有这些。定义一个函数:

template <class TLock, class TLockedOperation>
void WithLock(TLock *lock, const TLockedOperation &op)
{
    CSingleLock c(lock, TRUE);
    op();
}

该函数捕获了CSingleLock的正确用法。现在让用户这样做:

WithLock(&m_criticalSection, 
[&] {
        // do stuff, lock is held in this context.
    });

这对用户来说要困难得多。一开始语法看起来很奇怪,但是[&amp;]后面跟一个代码块意味着“定义一个不带args的函数,如果我按名称引用任何东西,它就是外面的东西的名称(例如,一个局部变量)包含函数)让我通过非const引用访问它,所以我可以修改它。)

答案 1 :(得分:5)

首先,Earwicker makes some good points - 你无法防止每次意外滥用这种结构。

但是对于您的具体情况,实际上可以避免这种情况。这是因为C ++确实对临时对象做了一个(奇怪的)区别:自由函数不能对非临时对象进行非常量引用。因此,为了避免突然进入和不存在的锁定,只需移动锁定代码从CSingleLock构造函数中转移到一个自由函数中(您可以让朋友避免将内部暴露为方法):

class CSingleLock {
    friend void Lock(CSingleLock& lock) {
        // Perform the actual locking here.
    }
};

仍然在析构函数中执行解锁。

使用:

CSingleLock myLock(&m_criticalSection, TRUE);
Lock(myLock);

是的,写起来稍微笨拙。但是现在,如果你尝试,编译器会抱怨:

Lock(CSingleLock(&m_criticalSection, TRUE));   // Error! Caught at compile time.

因为Lock()的非const ref参数无法绑定到临时参数。

也许令人惊讶的是,类方法可以在临时工作 - 这就是Lock()需要成为自由函数的原因。如果您将friend说明符和函数参数放在顶部代码段中以使Lock()成为方法,那么编译器将很乐意允许您编写:

CSingleLock(&m_criticalSection, TRUE).Lock();  // Yikes!

MS编译器注意:直到Visual Studio .NET 2003的MSVC ++版本错误地允许函数绑定到VC ++ 2005之前版本中的非const引用。This behaviour has been fixed in VC++ 2005 and above

答案 2 :(得分:3)

不,没有办法做到这一点。这样做会打破几乎所有依赖于创建无名临时代码的C ++代码。您对特定类的唯一解决方案是将其构造函数设置为私有,然后始终通过某种工厂构造它们。但我认为治愈比疾病更糟糕了!

答案 3 :(得分:2)

我不这么认为。

虽然这不是一件明智的事情 - 因为你已经发现了你的错误 - 这句话并没有“非法”。编译器无法知道方法的返回值是否“至关重要”。

答案 4 :(得分:2)

编译器不应该禁止创建临时对象,恕我直言。

特别是缩小矢量的情况,你真的需要创建临时对象。

std::vector<T>(v).swap(v);

虽然有点困难,但仍然需要进行代码审查和单元测试才能解决这些问题。

否则,这是一个穷人的解决方案:

CSingleLock aLock(&m_criticalSection); //Don't use the second parameter whose default is FALSE

aLock.Lock();  //an explicit lock should take care of your problem

答案 5 :(得分:0)

以下怎么样?略微滥用预处理器,但它很聪明,我认为它应该包括在内:

class CSingleLock
{
    ...
};
#define CSingleLock class CSingleLock

现在忘记将临时结果命名为错误,因为以下内容是有效的C ++:

class CSingleLock lock(&m_criticalSection, true); // Compiles just fine!

相同的代码,但省略名称,不是:

class CSingleLock(&m_criticalSection, true); // <-- ERROR!

答案 6 :(得分:0)

老问题,但我要补充两点。

通过定义与该类同名的宏函数,当有人忘记了变量名时,可以通过一条有用的消息触发静态断言。 live here

class CSingleLock {
 public:
  CSingleLock (std::mutex*, bool) { }
};

// must come after class definition
#define CSingleLock(...) static_assert(false, \
    "Temporary CSingleLock objects are forbidden, did you forget a variable name?")

won't match when there is a ariable name。但是,此doesn't help in the case of uniform initialization;您无法抓住CSingleLock{&m, true}PfhorSlayer's answer与统一初始化一起使用,因此使用起来更安全,但会产生更混乱的错误消息。我还是会推荐这种解决方案。不幸的是,所有这些宏解决方案fail when the type is in a namespace


另一种解决方案是使用[[nodiscard]]

生成编译器警告
class CSingleLock {
 public:
  [[nodiscard]] CSingleLock (std::mutex*, bool) { }
};

如果您创建一个临时clang will warn you话:

warning: ignoring temporary created by a constructor declared with 'nodiscard' attribute [-Wunused-value]
  CSingleLock(&m, true);
  ^~~~~~~~~~~~~~~~~~~~~

GCC 9.2似乎[[nodiscard]] on constructors有问题。如果您不使用结果,它仍为gives additional warnings。问题是fixed on head,在撰写本文时,这是wandbox上的gcc-10 20191217。

答案 7 :(得分:-1)

我看到在5年内没有人提出最简单的解决方案:

#define LOCK(x) CSingleLock lock(&x, TRUE);
...
void f() {
   LOCK(m_criticalSection);

现在只使用此宏来创建锁。没有机会再创造临时工!这样做的另一个好处是可以轻松扩展宏以在调试版本中执行任何类型的检查,例如检测不适当的递归锁定,记录文件和锁定行等等。