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