5

I have a struct that needs to have some instances declared volatile because they represent memory that is shared with a driver (i.e. the memory may be changed by a process outside my C++ program). The struct also needs to be trivially-copyable, because I will be sharing instances of it with some code that requires all its inputs to be trivially-copyable. These two requirements seem to mean that it is impossible for me to safely assign a new value to volatile instances of the struct.

Here's a simplified example of what I'm trying to do:

struct foo {
    uint16_t a;
    uint16_t b;
};

int main() {
    static_assert(std::is_trivially_copyable<foo>::value, "Oh no!");
    volatile foo vfoo;
    foo foo_value{10, 20};
    vfoo = foo_value;
}

If I try to compile this with g++, it fails on the line vfoo = foo_value with "Error: passing 'volatile foo' as 'this' argument discards qualifiers." According to this question, that's because the implicitly-defined assignment operator is not declared volatile, and I need to define a volatile copy-assignment operator in order to assign to a volatile object. However, if I do this:

struct foo {
    uint16_t a;
    uint16_t b;
    volatile foo& operator=(const foo& f) volatile {
        if(this != &f) {
            a = f.a;
            b = f.b;
        }
        return *this;
    }
}

Then the static assertion fails, because foo is no longer trivially-copyable if it has a user-defined assignment operator.

Since the compiler has apparently decided that I am not allowed to do this very simple action, I'm currently using this workaround:

int main() {
    static_assert(std::is_trivially_copyable<foo>::value, "Oh no!");
    volatile foo vfoo;
    foo foo_value{10, 20};
    memcpy(const_cast<foo*>(&vfoo), &foo_value, sizeof(foo));
    std::atomic_signal_fence(std::memory_order_acq_rel);
}

Obviously, casting away the volatile isn't a good idea, because that means the compiler is now allowed to violate the semantics volatile was supposed to enforce (i.e. every read and write in code is translated to an actual read or write to memory). I tried to mitigate this by replacing the assignment with a memcpy, which should mean the compiler can't optimize away the write (even if it thinks that the write will not be visible to the rest of the program), and by adding a memory fence after the assignment, which should mean the compiler can't choose to delay the write until much later.

Is this the best I can do? Is there a better workaround that will get closer to the correct semantics for volatile? Or is there a way to get the compiler to let me assign a new value to a volatile struct without making the struct non-trivially-copyable?

Edward
  • 5,942
  • 4
  • 38
  • 55
  • 3
    If a parallel process/thread can change the data then you have a synchronization issue. Volatile won't save you from that, you need some synchronization primitives. Isn't this an XY problem? – freakish Sep 19 '19 at 17:44
  • The only trivially copyable types are scalar types, trivially copyable classes, and arrays of such types/classes (possibly const-qualified, but **not** volatile-qualified). – Ted Lyngmo Sep 19 '19 at 17:45
  • I have already determined that volatile is the correct choice for this system. The volatile-tagged structs are getting passed to an RDMA network driver that issues an asynchronous request to post that region of memory in a one-sided send. Since the request is guaranteed to happen after I call the send function, I don't need additional synchronization, but I do need to guarantee that writes that occur in the code before I call the send function have actually been written to memory by the time I call the function. – Edward Sep 19 '19 at 17:48
  • have you considered overloading the assignment operator? – Wolfgang Brehm Sep 19 '19 at 18:01
  • Not an answer, but a rant - I noticed that `volatile` is treated like a second-class citizen in C++. This is indeed frustrating. – SergeyA Sep 19 '19 at 18:09
  • @Edward: "*Since the request is guaranteed to happen after I call the send function, I don't need additional synchronization*" So, how do you guarantee the visibility of the data? `volatile` doesn't do that. The network driver may update RAM, but unless you do something, the *cache* may still have old data in it. And `volatile` does not necessarily help with that. – Nicol Bolas Sep 19 '19 at 19:36
  • @SergeyA: "*I noticed that volatile is treated like a second-class citizen in C++.*" That's because it's the wrong tool. Or rather, it's in the wrong place. [The *object* isn't "volatile"; it's a particular access to it which a user wants to actually happen](https://wg21.link/P1152R0). This is what [P1382](https://wg21.link/P1382) is all about: providing a way to do what volatile says on a particular operation, rather than having it be the nature of the object. – Nicol Bolas Sep 19 '19 at 19:42
  • 1
    @NicolBolas quite interesting, I haven't seen it! It seems to me, volatile should be no different from atomic, right? It can be argued that it is not particular object which is atomic, rather, it is a particular access to it. Yet we do encode this information in the type. I am not sure myself how to view both things, what would be your opinion on it? – SergeyA Sep 19 '19 at 19:55

1 Answers1

5

If you don't necessarily need to use the assignment operator (i.e. if you consider the memcpy alternative viable) then you could write a non-operator assignment function instead:

volatile foo& volatile_assign(volatile foo& f, const foo& o) {
    if(&f != &o) {
        f.a = o.a;
        f.b = o.b;
    }
    return f;
}

You can use a member function if you so prefer.

I've written this based on your example, but consider whether the self assignment check is valid regarding volatile semantics. Shouldn't the same values be re-written? I don't think the case is even valid unless the object is actually non-volatile since otherwise we would be reading a volatile object through the non-volatile reference (maybe you need volatile qualification for the other operand as well).

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • This looks like a good solution, and you're right about the self-assignment check. I only put it in to conform to the format of the "standard" copy-assignment operator, but in the case of a volatile struct it will never be used (the second argument can't be the same object as `f` since it's non-volatile). – Edward Sep 21 '19 at 19:54