禁止创建临时对象

发布于 2024-07-21 09:40:06 字数 310 浏览 16 评论 0原文

在调试多线程应用程序中的崩溃时,我最终在以下语句中找到了问题:

CSingleLock(&m_criticalSection, TRUE);

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

While debugging crash in a multithreaded application I finally located the problem in this statement:

CSingleLock(&m_criticalSection, TRUE);

Notice that it is creating an unnamed object of CSingleLock class and hence the critical section object gets unlocked immediately after this statement. This is obviously not what the coder wanted. This error was caused by a simple typing mistake. My question is, is there someway I can prevent the temporary object of a class being created at the compile time itself i.e. the above type of code should generate a compiler error. In general, I think whenever a class tries to do some sort of resource acquisition then the temporary object of that class should not be allowed. Is there any way to enforce it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

呆头 2024-07-28 09:40:07

首先,Earwicker 提出了一些很好的观点——你可以' t 防止此结构的每次意外误用。

但对于你的具体情况,这实际上是可以避免的。 这是因为 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() 的非常量引用参数无法绑定到临时变量。

也许令人惊讶的是,类方法可以对临时变量进行操作——这就是为什么Lock()需要是一个自由函数。 如果您删除顶部代码片段中的 friend 说明符和函数参数以使 Lock() 成为一个方法,那么编译器将很乐意允许您编写:

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

MS编译器注意: Visual Studio .NET 2003 之前的 MSVC++ 版本错误地允许函数绑定到 VC++ 2005 之前版本中的非常量引用。 此行为已在 VC++ 2005 及更高版本中修复

First, Earwicker makes some good points -- you can't prevent every accidental misuse of this construct.

But for your specific case, this can in fact be avoided. That's because C++ does make one (strange) distinction regarding temporary objects: Free functions cannot take non-const references to temporary objects. So, in order to avoid locks that blip into and out of existence, just move the locking code out of the CSingleLock constructor and into a free function (which you can make a friend to avoid exposing internals as methods):

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

Unlocking is still performed in the destructor.

To use:

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

Yes, it's slightly more unwieldy to write. But now, the compiler will complain if you try:

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

Because the non-const ref parameter of Lock() cannot bind to a temporary.

Perhaps surprisingly, class methods can operate on temporaries -- that's why Lock() needs to be a free function. If you drop the friend specifier and the function parameter in the top snippet to make Lock() a method, then the compiler will happily allow you to write:

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

MS COMPILER NOTE: MSVC++ versions up to Visual Studio .NET 2003 incorrectly allowed functions to bind to non-const references in versions prior to VC++ 2005. This behaviour has been fixed in VC++ 2005 and above.

很糊涂小朋友 2024-07-28 09:40:07

我不这么认为。

虽然这不是明智之举 - 正如您在错误中发现的那样 - 该声明没有任何“非法”之处。 编译器无法知道该方法的返回值是否“重要”。

I don't think so.

While it's not a sensible thing to do - as you've found out with your bug - there's nothing "illegal" about the statement. The compiler has no way of knowing whether the return value from the method is "vital" or not.

太阳哥哥 2024-07-28 09:40:07

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

特别是像缩小向量这样的情况,您确实需要创建临时对象。

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

Compiler shouldn't disallow temporary object creation, IMHO.

Specially cases like shrinking a vector you really need temporary object to be created.

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

Though it is bit difficult but still code review and unit testing should catch these issues.

Otherwise, here is one poor man's solution:

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
失而复得 2024-07-28 09:40:07

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

No, there is no way of doing this. Doing so would break almost all C++ code which relies heavily on creating nameless temporaries. Your only solution for specific classes is to make their constructors private and then always construct them via some sort of factory. But I think the cure is worse than the disease!

忘年祭陌 2024-07-28 09:40:07

您可以使用 [[nodiscard]] 引发编译器警告

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

如果您创建临时 clang 将警告您说:

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

GCC 9.3 似乎有问题 [[nodiscard]] 在构造函数上不过。 如果您不使用结果,它仍然给出额外警告。 该问题已在 gcc 10+ 中修复,并且会产生类似(但不太具体)的警告。


另一种可能的解决方案:通过定义与类同名的宏函数,当有人忘记变量名称时,您可以触发静态断言并提供有用的消息。 住在这里

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?")

有变量名时不会匹配。 然而,这个在统一初始化的情况下没有帮助; 你无法捕获CSingleLock{&m, true}PfhorSlayer 的答案适用于统一初始化,因此使用起来更安全,但代价是错误消息更加混乱。 我仍然会推荐该解决方案而不是我的解决方案。 不幸的是,当类型位于命名空间中时,所有这些宏解决方案都会失败

You can cause a compiler warning using [[nodiscard]]

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

If you create a temporary clang will warn you saying:

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

GCC 9.3 seems to have a problem with [[nodiscard]] on constructors though. It still gives additional warnings if you don't use the result. The problem is fixed in gcc 10+ and it produces a similar (but less-specific) warning.


Another possible solution: by define a macro function with the same name as the class, you can trigger a static assertion with a helpful message when someone forgets the variable name. 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?")

The macro won't match when there is a variable name. However, this doesn't help in the case of uniform initialization; you can't catch CSingleLock{&m, true}. PfhorSlayer's answer works with uniform initialization so it is safer to use, at the cost of a more confusing error message. I would still reccomend that solution over mine. Unfortunately all these macro solutions fail when the type is in a namespace.

不奢求什么 2024-07-28 09:40:07

我看到五年来没有人想出最简单的解决方案:

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

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

I see that in 5 years nobody has come up with the most simple solution:

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

And now only use this macro for creating locks. No chance to create temporaries any more! This has the added benefit that the macro can be easily augmented to perform any kind of checking in debug builds, for example detecting inappropriate recursive locking, recording file and line of the lock, and much more.

遗弃M 2024-07-28 09:40:07

那么下面的呢? 稍微滥用预处理器,但它足够聪明,我认为应该包含它:

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

现在忘记命名临时结果会导致错误,因为虽然以下是有效的 C++:

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

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

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

What about the following? Slightly abuses the preprocessor, but it's clever enough that I think it should be included:

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

Now forgetting to name the temporary results in an error, because while the following is valid C++:

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

The same code, but omitting the name, is not:

class CSingleLock(&m_criticalSection, true); // <-- ERROR!
杯别 2024-07-28 09:40:06

编辑: 正如 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 中,还有另一种方法可以完成这一切,即使用 lambda。 定义一个函数:

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.
    });

这对于用户来说更难搞砸。 语法一开始看起来很奇怪,但是 [&] 后面跟着一个代码块意味着“定义一个不带参数的函数,如果我按名称引用任何东西,并且它是外部某个东西的名称(例如,包含函数)让我通过非常量引用访问它,这样我就可以修改它。)

Edit: As j_random_hacker notes, it is possible to force the user to declare a named object in order to take out a lock.

However, even if creation of temporaries was somehow banned for your class, then the user could make a similar mistake:

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

// do other stuff, assuming lock is held

Ultimately, the user has to understand the impact of a line of code that they write. In this case, they have to know that they're creating an object and they have to know how long it lasts.

Another likely mistake:

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

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

Which would lead you to ask "Is there any way I can stop the user of my class from allocating it on the heap"? To which the answer would be the same.

In C++0x there will be another way to do all this, by using lambdas. Define a function:

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

That function captures the correct usage of CSingleLock. Now let users do this:

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

This is much harder for the user to screw up. The syntax looks weird at first, but [&] followed by a code block means "Define a function that takes no args, and if I refer to anything by name and it is the name of something outside (e.g. a local variable in the containing function) let me access it by non-const reference, so I can modify it.)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文