0

I need to declare a class object as volatile:

int main() {
    volatile FixedQueue<Job, 5> q{};
    q.push({ 0x42, 0x55 });
}

I learned from this post that all member functions should be volatile so they can be called from a volatile object:

template <typename T, size_t N>
class FixedQueue {
public:
    FixedQueue() = default;

    constexpr void push(const T& obj) volatile {
        m_back = plusOne(m_back);
        *m_back = obj;
    }
private:
    T* plusOne(T* ptr) volatile { 
        if (ptr + 1 == m_data + N) return m_data;
        return ptr + 1;
    }

    T m_data[N]{};
    T* m_back{ m_data };
};

However, the compiler says invalid conversion from 'volatile Job*' to 'Job*', referring to return m_data, which leads me to believe that m_data is of type volatile T[]. To deal with this, I change plusOne()'s return type to volatile T*.

The error reappears but on the line m_back = plusOne(m_back);, which leads me to believe that m_back is of type volatile T*. I change plusOne's parameter type to volatile T*.

The error doesn't go away.

Where is this conversion happening, and what am I doing wrong?


Complete program (note that this is a simplified example of my actual program):

#include <stddef.h>
#include <stdint.h>

template <typename T, size_t N>
class FixedQueue {
public:
    FixedQueue() = default;

    constexpr void push(const T& obj) volatile {
        m_back = plusOne(m_back);
        *m_back = obj;
    }
private:
    volatile T* plusOne(volatile T* ptr) volatile { 
        if (ptr + 1 == m_data + N) return m_data;
        return ptr + 1;
    }

    T m_data[N]{};
    T* m_back{ m_data };
};

struct Job{ uint8_t byte; uint8_t addr; };

int main() {
    volatile FixedQueue<Job, 5> q{};
    q.push({ 0x42, 0x55 });
}
glibg10b
  • 338
  • 1
  • 11
  • 2
    There's an elephant in the room: why do you need a `volatile` queue? – Passer By Jul 10 '22 at 08:38
  • @PasserBy The class object can be read and modified by AVR interrupts at any time. If it weren't volatile, the optimizer would make incorrect assumptions – glibg10b Jul 10 '22 at 08:41
  • The type `'volatile Job*'` makes me suspect that you try to use `volatile` for some threading problem. This is not what `volatile` is used for. So now this looks like an [XY-problem](https://xyproblem.info/) where you ask about how to solve a problem with the attempted solution, instead of asking about the original problem. – BoP Jul 10 '22 at 08:44
  • I'm not familiar with hardware, but are you sure what you're looking for isn't atomics? `volatile` doesn't, in any way whatsoever, guarantee thread safety in C++. AVR compilers may have their own guarantees. – Passer By Jul 10 '22 at 08:44
  • @BoP I always turn off interrupts before reading and modifying the queue, so there can't be threading problems. I use the volatile keyword to tell the compiler that reading and writing has to be done immediately, regardless of where the queue is used within the main thread. See https://www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/volatile/ – glibg10b Jul 10 '22 at 08:49
  • @PasserBy Atomics are only necessary for preventing modifications to your object from being interrupted. That's not what I'm using `volatile` for. – glibg10b Jul 10 '22 at 08:50
  • 1
    All members of a `volatile` class gets qualified with `volatile` as well. The problem is, a `volatile` pointer is `T* volatile` which isn't `volatile T*`, hence your errors. You're likely better off just having `volatile` members instead. – Passer By Jul 10 '22 at 08:52
  • 1
    I think making `FixedQueue` `volatile` is a mistake. It's not `volatile` - only what it's pointing at/reading from is - if I understand the setup correctly. – Ted Lyngmo Jul 10 '22 at 08:57
  • @Ted I think you're right. If I don't get a better answer soon, I'll make the members volatile and not the object. Too bad the class wouldn't be reusable in situations where volatile isn't needed – glibg10b Jul 10 '22 at 09:00
  • 1
    @glibg10b Can't you just remove all `volatile`s from the class template and instantiate it with `FixedQueue` if you want an array of pointers to `volatile` `int`s for example? – Ted Lyngmo Jul 10 '22 at 09:04
  • @Ted Then I get `cannot bind non-const lvalue reference of type 'const volatile Job&' to an rvalue of type 'const volatile Job'` at the call to push(), which doesn't make sense to me because the parameter is a const reference. Should I start a new question for this? – glibg10b Jul 10 '22 at 09:13
  • Perhaps I'm oversimplifying things. I was thinking something [like this](https://godbolt.org/z/4n8rY9TjK) – Ted Lyngmo Jul 10 '22 at 09:23
  • @Ted Please forgive my ignorance, but how could that be adapted to work with objects of type Job? Your method works with pointers and integers, but not with struct objects – glibg10b Jul 10 '22 at 09:35
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246310/discussion-between-glibg10b-and-ted-lyngmo). – glibg10b Jul 10 '22 at 09:36
  • You *can* reuse `FixedQueue` if you have `volatile FixedQueue q{};`. A slight problem here is that a `volatile` struct is not assignable by default so `*m_back = obj;` will fail. You need to add a volatile assignment operator to `Job`. See https://godbolt.org/z/MWTh5cc8q – n. m. could be an AI Jul 10 '22 at 13:51

1 Answers1

0

volatile is the wrong thing, or rather insuficient.

volatile indeed means that every read and write is observable and must not be optimized away. But it also means it can't be optimized away.

What it does not mean is that it can't be interrupted. So in your code

m_back = plusOne(m_back);
*m_back = obj;

there is no guarantee an interrupt won't fire between the first and second line or in the middle of plusOne. If obj is larger than the CPU can write in one go there might even be an interrupt in the middle of writing obj.

You can only use volatile with objects that are naturally atomic on the CPU and for operations that are atomic. Beyond that you need more.

For multithreading this is solved with atomic<T>, ensuring different threads don't interfere with each other. But that doesn't work for interrupts.

To make your queue interrupt safe you have to add calls to disable/enable interrupts to make larger operations become atomic. The smartest way to do that would be like the lock_guard with RAII. If your runtime doesn't already have something for it create a class CriticalSection that disables interrupts in the constructor and reenables them in the destructor. Bonus points if you make it reentrant using a static int count = 0;.

You can mark your member variables volatile to guarantee values are always read from ram and written back to ram but as said that prevents any optimization. A better way is to include a memory barrier in the CriticalSection constructor and destructor. This will allow the compiler to optimize while within the critical section where interrupts are disabled but not across those barriers. So everything is read from ram when entering the critical section and written back before leaving but while within things can use registers.

You can use atomic load/store for the count and use the existing barriers for that purpose.


Update: This is how the CriticalSection could look like:

class CriticalSection {
    public:
    CriticalSection() {
        asm("cli":::"memory");
        ++count;
    }
    ~CriticalSection() {
        if (--count == 0) {
            asm("sei":::"memory");
        } else {
            asm("":::"memory");
        }
    }
    private:
    static inline int count = 0;
};

The cli and sei make operations atomic and the memory clobber produces a compiler barrier that prevents optimizations of reads and writes across it. This eliminates the need for any volatile.

Usage is like this:

constexpr void push(const T& obj) {
    CiritcalSection t;
    m_back = plusOne(m_back);
    *m_back = obj;
}
Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • "What it does not mean is that it can't be interrupted" -- That's what I'm using cli() and sei() for -- they globally disable and enable interrupts, respectively. `volatile` is just to prevent optimization. "there is no guarantee an interrupt won't fire between the first and second line" -- from [my chip's datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf): "No interrupt will be executed after the CLI instruction, even if it occurs simultaneously with the CLI instruction". – glibg10b Jul 10 '22 at 14:41
  • @glibg10b You should have left that in the code. – Goswin von Brederlow Jul 10 '22 at 14:42
  • This answer doesn't answer the question. In the future, please make sure what a question is about and **read the comments underneath it** before attempting to answer it. – glibg10b Jul 10 '22 at 14:42
  • I believe it does. Remove the volatile everywhere and add the `cli` / `sei` and barriers and you have something that works and works well. – Goswin von Brederlow Jul 10 '22 at 14:46
  • @Goshwin cli() and sei() don't account for compiler optimizations. It's not in the code because they're used *outside* of the class and the question is about conversions between volatile types. Notice that the question doesn't have the [AVR] tag and contains no mentions of interrupts or threads – glibg10b Jul 10 '22 at 14:52
  • @glibg10b My answer doesn't mention AVR either. It does targets interrupts though as that is the use case you stated in the comments. – Goswin von Brederlow Jul 10 '22 at 15:02