C/C++:消除volatile是否有害?

15

(与此问题相关的问题是Is It Safe to Cast Away volatile?,但并不完全相同,因为那个问题涉及到一个特定的实例)

是否存在一种情况,其中强制转换volatile被认为是不危险的?

(一个特别的例子:如果有一个声明的函数

void foo(long *pl);

我需要实现这个功能

void bar(volatile long *pl);

如果我的一部分实现需要bar()调用foo(pl),但似乎我无法让其正常工作,因为编译foo()和调用bar()的编译所做出的假设是不兼容的。


作为推论,如果我有一个volatile变量v,我想要调用别人的函数void foo(long *pl)并将v的地址传递给它,并且那个人告诉我这样做是安全的,我的直觉告诉我告诉他们是错误的,因为没有办法保证这是安全的,如果他们想要支持使用volatile变量,他们应该将声明更改为void foo(volatile long *pl)。哪个正确?


5
void bar(volatile long* pl) { long x = *pl; foo(&x); *pl = x; }这段代码的意思是:定义了一个函数bar,它接受一个指向长整型的指针pl,并使用volatile关键字来确保编译器不会对变量进行优化。在函数内部,将pl指向的值(也就是x)赋值给局部变量x,然后调用函数foo,并将x的地址传递给它。最后,将x的值写回到pl指向的位置。 - James McNellis
1
为什么不重载foo函数,以便在需要被bar调用时有一个易变友好的版本呢? - AJG85
1
@James:不行,因为那个volatile变量的语义可能无法产生正确的结果。例如,如果foo()函数应该是原子递增的,那么有可能从两个线程调用bar()并且失去其中一个递增。 - Jason S
1
@Jason S:在这种情况下,要么按照James所说的更改bar,要么使用正确的约定。此外,在C++中,volatile不能提供原子变量,因此无论如何都不足够。 - AJG85
2
@Jason:如果语义是James的解决方案不正确,则foo可能已经应该使用volatile指针。例如,如果foo是原子递增,那么强制转换掉volatile应该是无害的,因为原子操作已经提供了比volatile更强的保证... - R.. GitHub STOP HELPING ICE
显示剩余4条评论
3个回答

21
如果变量被声明为volatile,那么像将一个被声明为const的变量转换成非const类型一样,在强制类型转换时消除volatile是未定义行为。请参阅C标准附录J.2:行为在以下情况下是未定义的:
...
- 尝试使用具有非const限定类型的lvalue修改使用const限定类型定义的对象(6.7.3)。 - 尝试使用具有非volatile限定类型的lvalue引用使用volatile限定类型定义的对象(6.7.3)。
然而,如果你只有一个指向非volatile变量的volatile指针或者volatile引用,那么你可以自由地消除volatile
volatile int i=0;
int j=0;

volatile int* pvi=&i; // ok
volatile int* pvj=&j; // ok can take a volatile pointer to a non-volatile object

int* pi=const_cast<int*>(pvi); // Danger Will Robinson! casting away true volatile
int* pj=const_cast<volatile int*>(pvj); // OK
*pi=3; // undefined behaviour, non-volatile access to volatile variable
*pj=3; // OK, j is not volatile

C语言也是一样吗? - MCG
我为C参考点赞,但我认为你在volatile int* pvj=&j; // ok can take a volatile pointer to a non-volatile object处有一些错误。你正在将一个非易失性int的引用分配给一个易失性int的指针,所以除非那会导致j成为易失性,否则我认为存在问题。此外,你谈到了一个易失性指针,但我在你的代码中没有看到任何东西。易失性指针将是这样的int* volatileint volatile* volatile(相当于volatile int* volatile)。 - abc

9

一旦值确实不再是volatile,抛弃volatile就可以了。在SMP/多线程环境中,此情况可能发生在获取锁之后(并通过内存屏障传递,通常隐含在获取锁时进行)。

因此,这种情况的典型模式如下:

 volatile long *pl = /*...*/;

 //
 {
      Lock scope(m_BigLock);   /// acquire lock
      long *p1nv = const_cast<long *>(p1);

      // do work
 } // release lock and forget about p1nv!

但是还有很多场景中,值不再是易变的。我不会在这里提出其他建议,因为我相信如果你知道自己在做什么,你自己可以想出它们。否则,锁定方案似乎足够坚实,可以作为一个例子。


2
你需要使用 const_cast 来去除 volatile,而不是使用 static_cast - ildjarn
1
谢谢。我之前没有考虑过这种情况,即在某些时间间隔内,易失性变量可以具有非易失性保证。 - Jason S

5

使用foo(long *pl)签名,程序员声明他们不希望在foo执行期间外部更改指向的long值。传递指向正在并发修改的long值的指针可能会导致错误行为,如果编译器由于缺少寄存器而选择不将第一次解引用的值存储在堆栈上,则会多次解引用指针。例如,在以下代码中:

void foo(long *pl) {

    char *buf = (char *) malloc((size_t) *pl);

    // ... busy work ...

    // Now zero out buf:
    long l;
    for (l = 0; l < *pl; ++l) {
        buf[l] = 0;
    }

    free(buf);
}

foo在“清空缓冲区”步骤中可能会溢出,如果在繁忙的工作期间增加了long值。

如果foo()函数应该原子地增加pl指向的long值,则函数采用long *pl而不是volatile long *pl是不正确的,因为函数明显需要对long值的访问是一个序列点。如果foo()只是原子地增加,那么函数可能会工作,但是它将不正确。

这个问题已经有两种解决方案被提出:

  1. Wrap foo taking long * in an overload taking volatile long *:

    inline void foo(volatile long *pvl) {
        long l = *pvl;
        foo(&l);
        *pvl = l;
    }
    
  2. Change the declaration of foo to void foo(volatile long *pvl).


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接