volatile类类型与内置类型的丢弃值表达式行为差异问询
Great question—this is a super nuanced corner of C++ where volatile behavior splits between built-in and class types, and while your reading of N4659 is headed in the right direction, there’s a key rule distinction that changes the expected behavior for some cases. Let’s break this down clearly.
First, the Standard Rule (N4659 [expr]/12)
When dealing with a discarded-value expression (like standalone ai;, as;, or as_bad;), the standard lays out specific logic for whether an lvalue-to-rvalue conversion is applied:
When an expression is evaluated in a context where a discarded-value expression is expected, it is evaluated as follows:
- If the expression is a prvalue, the temporary materialization conversion is applied.
- Otherwise, if the expression is an lvalue, the lvalue-to-rvalue conversion is performed if and only if the expression is a glvalue of volatile-qualified type and it is one of the following:
- An lvalue of type cv
bool;- A class type or array of class type where the cv-unqualified version of the type has a user-declared destructor or a user-declared copy constructor.
- [Note: This ensures that volatile-qualified objects are accessed correctly. — end note]
- The value of the expression is discarded.
Let’s Walk Through Your Three Cases
To make this concrete, let’s assume example definitions matching your scenario:
// Class with user-declared destructor + volatile copy constructor struct S { S() = default; S(const volatile S&) { /* Volatile copy logic here */ } ~S() = default; // User-declared destructor triggers the rule }; // "Plain" class with no user-declared special members struct SBad { SBad() = default; // No user-declared copy constructor or destructor }; volatile int ai; volatile S as; volatile SBad as_bad;
1. ai; (Volatile Built-in Type)
This is a volatile-qualified lvalue, but it doesn’t fall into either case (1) or (2) above (it’s not bool, nor a class type). No lvalue-to-rvalue conversion is performed. This means the expression doesn’t actually read the memory value of ai—it’s just a no-op lvalue reference, with no access to the volatile object’s state.
2. as; (Volatile Class with User-Declared Special Members)
This fits case (2) perfectly: it’s a volatile class type, and S has a user-declared destructor. The lvalue-to-rvalue conversion is mandatory. For class types, this conversion requires creating a temporary prvalue by invoking the matching copy constructor—here, that’s S(const volatile S&), your volatile copy constructor. After that, temporary materialization converts the prvalue to a temporary object (which is immediately destroyed since it’s a discarded value). Your expectation here was spot-on.
3. as_bad; (Volatile Class with No User-Declared Special Members)
Even though this is a volatile class type, it doesn’t meet case (2)’s requirement (no user-declared destructor or copy constructor). No lvalue-to-rvalue conversion happens, so no copy constructor is called. Like the built-in type case, this is just an lvalue reference with no access to the volatile object’s state.
Why the Difference?
The standard draws this line to balance correctness and performance/intent:
- For classes with user-declared destructors or copy constructors, the assumption is that the class has custom resource management or volatile-specific logic that requires proper access to the object’s state. Enforcing the lvalue-to-rvalue conversion ensures that the volatile object’s state is read (via the copy constructor) as intended.
- For built-in types and "plain" classes (no custom special members), volatile is typically used for low-level memory control (e.g., memory-mapped I/O). Skipping the lvalue-to-rvalue conversion avoids unnecessary memory reads that could disrupt hardware interactions or waste cycles, while leaving control to the programmer if they explicitly want to read the value.
内容的提问来源于stack exchange,提问作者Oliv

