C/С++: отбрасывание летучих считается вредным?
(связанный с этим вопросом Безопасно ли исключить изменчивость?, но не совсем то же самое, что и этот вопрос относится к конкретному экземпляру)
Есть ли случай, когда отбрасывание volatile
не считается опасной практикой?
(один конкретный пример: если есть объявленная функция
void foo(long *pl);
и я должен реализовать
void bar(volatile long *pl);
с частью моей реализации, требующей bar() для вызова foo (pl), тогда кажется, что я не могу заставить это работать так, как есть, потому что предположения, сделанные компиляцией foo() и компиляцией вызывающий bar() несовместимы.)
В качестве следствия, если у меня есть переменная volatile
v, и я хочу называть foo(&v)
с кем-то еще функцией void foo(long *pl)
, и этот человек говорит мне, что это безопасно, я могу просто наложить указатель перед вызовом, мой инстинкт заключается в том, чтобы сказать им, что они ошибаются, потому что нет способа гарантировать это, и что они должны изменить объявление на void foo(volatile long *pl)
, если они хотят поддерживать использование изменчивых переменных. Кто из нас прав?
Ответы
Ответ 1
Если переменная объявлена volatile
, то она undefined поведение отбрасывает volatile
, так же как поведение undefined для приведения от const
от объявленной переменной const
. См. Приложение J.2 стандарта C:
Поведение undefined в следующих случаях:
...
- предпринимается попытка изменить объект, определенный с использованием типа const использование lvalue с неконстантно-квалифицированным типом (6.7.3).
- делается попытка обратиться к объекту, определенному с использованием нестабильного типа, через использование значения lvalue с типом энергонезависимого типа (6.7.3).
Если, однако, у вас есть указатель volatile
или volatile
для переменной non 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
Ответ 2
Отключение волатильности было бы нормально, как только значение фактически перестало быть изменчивым. В ситуациях SMP/многопоточности это может стать истинным после приобретения блокировки (и передачи барьера памяти, который чаще всего подразумевается при приобретении блокировки).
Таким образом, типичный шаблон для этого был бы
volatile long *pl = /*...*/;
//
{
Lock scope(m_BigLock); /// acquire lock
long *p1nv = const_cast<long *>(p1);
// do work
} // release lock and forget about p1nv!
Но я мог бы придумать ряд других сценариев, в которых значения перестают быть волатильными. Я не буду предлагать их здесь, так как я уверен, что вы сами можете придумать их, если вы знаете, что делаете. В противном случае сценарии блокировки кажутся достаточно прочными, чтобы обеспечить в качестве примера
Ответ 3
С сигнатурой foo(long *pl)
программист объявляет, что они не ожидают, что значение point-to long
изменится внешне во время выполнения foo
. Передача указателя на значение 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
может перехватить буфер на шаге "zero out buf", если значение long
увеличивается, когда выполняется занятая работа.
Если функция foo()
должна атомически увеличивать значение long
, на которое указывает pl
, то было бы неверно, чтобы функция принимала long *pl
, а не volatile long *pl
, потому что функция явно требует, чтобы доступ к значению long
будет точкой последовательности. Если foo()
только с атомарным увеличением, функция может работать, но это было бы неправильно.
В комментариях уже были предложены два решения этой проблемы:
-
Оберните foo
, взяв long *
при перегрузке volatile long *
:
inline void foo(volatile long *pvl) {
long l = *pvl;
foo(&l);
*pvl = l;
}
-
Измените объявление foo
на void foo(volatile long *pvl)
.