I'm pretty new to C++ and recently I ran across some info on what it means for a variable to be volatile. As far as I understood, it means a read or write to the variable can never be optimized out of existence.
However a weird situation arises when I declare a volatile variable that isn't 1, 2, 4, 8 bytes large: the compiler(gnu with C++11 enabled) seemingly ignores the volatile specifier
#define expand1 a, a, a, a, a, a, a, a, a, a
#define expand2 // ten expand1 here, expand3 to expand5 follows
// expand5 is the equivalent of 1e+005 a, a, ....
struct threeBytes { char x, y, z; };
struct fourBytes { char w, x, y, z; };
int main()
{
// requires ~1.5sec
foo<int>();
// doesn't take time
foo<threeBytes>();
// requires ~1.5sec
foo<fourBytes>();
}
template<typename T>
void foo()
{
volatile T a;
// With my setup, the loop does take time and isn't optimized out
clock_t start = clock();
for(int i = 0; i < 100000; i++);
clock_t end = clock();
int interval = end - start;
start = clock();
for(int i = 0; i < 100000; i++) expand5;
end = clock();
cout << end - start - interval << endl;
}
Their timings are
foo<int>(): ~1.5s
foo<threeBytes>(): 0
I've tested it with different variables (user-defined or not) that is 1 to 8 bytes and only 1, 2, 4, 8 takes time to run. Is this a bug only existing with my setup or is volatile a request to the compiler and not something absolute?
PS the four byte versions always take half the time as others and is also a source of confusion
The struct version will be optimized out probably, as the compiler realizes that there's no side effects (no read or write into the variable a), regardless of the volatile. You basically have a no-op, a;, so the compiler can do whatever it pleases it; it is not forced to unroll the loop or to optimize it out, so the volatile doesn't really matter here. In the case of ints, there seems to be no optimizations, but this is consistent with the use case of volatile: you should expect non-optimizations only when you have a possible "access to an object" (i.e. read or write) in the loop. However what constitutes "access to an object" is implementation-defined (although most of the time it follows common-sense), see EDIT 3 at the bottom.
Toy example here:
#include <iostream>
#include <chrono>
int main()
{
volatile int a = 0;
const std::size_t N = 100000000;
// side effects, never optimized
auto start = std::chrono::steady_clock::now();
for (std::size_t i = 0 ; i < N; ++i)
++a; // side effect (write)
auto end = std::chrono::steady_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;
// no side effects, may or may not be optimized out
start = std::chrono::steady_clock::now();
for (std::size_t i = 0 ; i < N; ++i)
a; // no side effect, this is a no-op
end = std::chrono::steady_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;
}
EDIT
The no-op is not actually optimized out for scalar types, as you can see in this minimal example. For struct's though, it is optimized out. In the example I linked, clang doesn't optimize the code with no optimization, but optimizes both loops with -O3. gcc doesn't optimize out the loops either with no optimizations, but optimizes only the first loop with optimizations on.
EDIT 2
clang spits out an warning: warning: expression result unused; assign into a variable to force a volatile load [-Wunused-volatile-lvalue]. So my initial guess was correct, the compiler can optimize out no-ops, but it is not forced. Why does it do it for structs and not scalar types is something that I don't understand, but it is the compiler's choice, and it is standard compliant. For some reason it gives this warning only when the no-op is a struct, and doesn't give the warning when it's a scalar type.
Also note that you don't have a "read/write", you only have a no-op, so you shouldn't expect anything from volatile.
EDIT 3
From the golden book (C++ standard)
7.1.6.1/8 The cv-qualifiers [dcl.type.cv]
What constitutes an access to an object that has volatile-qualified
type is implementation-defined. ...
So it is up to the compiler to decide when to optimize out the loops. In most cases, it follows the common sense: when reading or writing into the object.
This question is a lot more interesting than it first appears (for some definition of "interesting"). It looks like you've found a compiler bug (or intentional nonconformance), but it isn't quite the one you are expecting.
According to the standard, one of your foo calls has undefined behavior, and the other two are ill-formed. I'll first explain what should happen; the relevant standard quotes can be found after the break. For our purposes, we can just analyze the simple expression statement a, a, a; given volatile T a;.
a, a, a in this expression statement is a discarded-value expression ([stmt.expr]/p1). The type of the expression a, a, a is the type of the right operand, which is the id-expression a, or volatile T; since a is an lvalue, so is the expression a, a, a ([expr.comma]/p1). Thus, this expression is an lvalue of a volatile-qualified type, and it is a "comma expression where the right operand is one of these expressions" - in particular, an id-expression - and therefore [expr]/p11 requires the lvalue-to-rvalue conversion be applied to the expression a, a, a. Similarly, inside a, a, a, the left expression a, a is also a discarded-value expression, and inside this expression the left expression a is also a discarded-value expression; similar logic shows that [expr]/p11 requires the lvalue-to-rvalue conversion be applied to both the result of the expression a, a and the result of the expression a (the leftmost one).
If T is a class type (either threeBytes or fourBytes), applying the lvalue-to-rvalue conversion entails creating a temporary by copy-initialization from the volatile lvalue a ([conv.lval]/p2). However, the implicitly declared copy constructor always takes its argument by a non-volatile reference ([class.copy]/p8); such a reference cannot bind to a volatile object. Therefore, the program is ill-formed.
If T is int, then applying the lvalue-to-rvalue conversion yields the value contained in a. However, in your code, a is never initialized; this evaluation therefore produces an indeterminate value, and per [dcl.init]/p12, results in undefined behavior.
Standard quotes follows. All are from C++14:
[expr]/p11:
In some contexts, an expression only appears for its side effects.
Such an expression is called a discarded-value expression. The
expression is evaluated and its value is discarded. The
array-to-pointer (4.2) and function-to- pointer (4.3) standard
conversions are not applied. The lvalue-to-rvalue conversion (4.1) is
applied if and only if the expression is a glvalue of
volatile-qualified type and it is one of the following:
( expression ), where expression is one of these expressions,
id-expression (5.1.1),
[several inapplicable bullets omitted], or
comma expression (5.18) where the right operand is one of these expressions.
[ Note: Using an overloaded operator causes a function call; the
above covers only operators with built-in meaning. If the lvalue is of
class type, it must have a volatile copy constructor to initialize the
temporary that is the result of the lvalue-to-rvalue conversion. —end
note ]
[expr.comma]/p1:
A pair of expressions separated by a comma is evaluated left-to-right;
the left expression is a discarded-value expression (Clause 5) [...] The type
and value of the result are the type and value of the right operand;
the result is of the same value category as its right operand [...].
[stmt.expr]/p1:
Expression statements have the form
expression-statement:
expression_opt;
The expression is a discarded-value expression (Clause 5).
[conv.lval]/p1-2:
1 A glvalue (3.10) of a non-function, non-array type T can be
converted to a prvalue. If T is an incomplete type, a program that
necessitates this conversion is ill-formed. If T is a non-class
type, the type of the prvalue is the cv-unqualified version of T.
Otherwise, the type of the prvalue is T.
2 [some special rules not relevant here] In all other cases, the
result of the conversion is determined according to the following
rules:
[inapplicable bullet omitted]
Otherwise, if T has a class type, the conversion copy-initializes a temporary of type T from the glvalue and the result of the
conversion is a prvalue for the temporary.
[inapplicable bullet omitted]
Otherwise, the value contained in the object indicated by the glvalue is the prvalue result.
[dcl.init]/p12:
If no initializer is specified for an object, the object is
default-initialized. When storage for an object with automatic or
dynamic storage duration is obtained, the object has an indeterminate
value, and if no initialization is performed for the object, that
object retains an indeterminate value until that value is replaced
(5.17). [...] If an indeterminate value is produced by an evaluation,
the behavior is undefined except in the following cases: [certain
inapplicable exceptions related to unsigned narrow character types]
[class.copy]/p8:
The implicitly-declared copy constructor for a class X will have the
form
X::X(const X&)
if each potentially constructed subobject of a class type M (or
array thereof) has a copy constructor whose first parameter is of type
const M& or const volatile M&. Otherwise, the implicitly-declared
copy constructor will have the form
X::X(X&)
volatile doesn't do what you think it does.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html
If you're relying on volatile outside of the three very specific uses Boehm mentions on the page I linked, you're going to get unexpected results.
Related
#include <iostream>
constexpr int func2(int const& id){
return id;
}
template<int v>
struct Test{
};
int main(){
const int v = 0;
Test<func2(v)> c;
}
Consider the above code,I just don't understand why the code is well-formed.My pointview is that the name v is used as a glvalue when evalute expression func2,becuase the parameter of func2 is of reference type,the v need to be bound to the id-expression id.So we look at the requirement of a glvalue constant expression,here are quotes about that.
A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints.
We ignore the case of prvalue,because here v is used as a glvalue.
An entity is a permitted result of a constant expression is:
An entity is a permitted result of a constant expression if it is an object with static storage duration that is either not a temporary object or is a temporary object whose value satisfies the above constraints, or it is a function.
In my program portion,The const int v = 0; does not have static storage duration,it just has automatic storage duration.So when evaluting the expression func2(v) to determine whether it is a constant expression,Firstly,the v must be a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression,therefore,why the program is well-formed here?If I lose any important quote,Please correct me.
We ignore the case of prvalue, because here v is used as a glvalue
Is it though? This is an example from cppreference:
void test() {
static const int a = std::random_device{}();
constexpr const int& ra = a; // OK: a is a glvalue constant expression
constexpr int ia = a; // Error: a is not a prvalue constant expression
const int b = 42;
constexpr const int& rb = b; // Error: b is not a glvalue constant expression
constexpr int ib = b; // OK: b is a prvalue constant expression
}
And yes, const int b = 42 is rather weird here because technically speaking, you can bind b to const int&, const_cast the const away and assign a runtime value to it. However, considering what is an integral constant expression and what are the requirements of a const object it makes perfect sense:
Integral constant expression is an expression of integral or unscoped
enumeration type implicitly converted to a prvalue, where the
converted expression is a core constant expression. If an expression
of class type is used where an integral constant expression is
expected, the expression is contextually implicitly converted to an
integral or unscoped enumeration type.
Variable b sure does look like something you could implicitly convert to a prvalue constant expression because it basically serves as an alias for literal 42 in this context and integer literals are prvalues by definition.
Now for the problematic part - this:
const object - an object whose type is const-qualified, or a
non-mutable subobject of a const object. Such object cannot be
modified: attempt to do so directly is a compile-time error, and
attempt to do so indirectly (e.g., by modifying the const object
through a reference or pointer to non-const type) results in undefined
behavior.
And:
A core constant expression is any expression whose evaluation would
not evaluate any one of the following:
...
an expression whose evaluation leads to any form of core language
undefined behavior (including signed integer overflow, division by
zero, pointer arithmetic outside array bounds, etc). Whether standard
library undefined behavior is detected is unspecified.
Means that as soon as you start doing funny things with that b, you can expect anything to happen. For example, this is what I tried doing to your code in latest MSVC with all standard-conformance options switched on:
#include <iostream>
#include <random>
constexpr int func2(int const& id) {
return id;
}
template<int v>
struct Test {
long array[v];
};
int main() {
const int v = 0;
const int& ref = v;
const_cast<int&>(ref) = std::random_device()() % std::numeric_limits<int>::max();
Test<func2(v)> c;
return 0;
}
With language extensions turned on I got a C4200: nonstandard extension used : zero-sized array in struct/union warning. After switching them off, the program wouldn't compile. And when I deleted the array part from the struct it started compiling again.
I try to answer this question.why the func2(v) is a constant expression,Becuase For expression func2(v),when evaluting this postfix-expression,there's no requirement that v must be a glvalue constant expression in the list of "would evaluate one of the following expressions:",Even,these rules does not mandate that the one expression within a potentially core constant expression would be a glvalue constant expression,only require the expression does not violate the listed requirement.So let's continue,When initialization of the parameter,It's another rule here:
A full-expression is:
[...]
an init-declarator or a mem-initializer, including the constituent expressions of the initializer
So,when evalute this full-expression,it only does not violate these listed condition,then the func2(v) would be evaluted as a constant expression,So let's look at these rules:
an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either
it is initialized with a constant expression or,
its lifetime began within the evaluation of e;
For id-expression id,its preceding initialization is the corresponding argument,Because of this rule:
When a function is called, each parameter ([dcl.fct]) shall be initialized ([dcl.init], [class.copy], [class.ctor]) with its corresponding argument.
So,the first condition is true.And "it is initialized with a constant expression" is false,the condition "its lifetime began within the evaluation of e" is true.In conlusion,the expression func2(v) indeed a constant expression
The code below compiles in GCC, clang and VS2017 and the expression a->i in the return statement is replaced by its constant value 1. Is it correct to say that this is valid because a is not odr-used in the expression a->i?.
struct A
{
static const int i = 1;
};
int f()
{
A *a = nullptr;
return a->i;
}
PS: I believe a is not odr-used in the expression a->i because it satisfies the "unless" condition in [basic.def.odr]/4, as follows:
A variable x whose name appears as a potentially-evaluated
expression ex is odr-used by ex unless applying the
lvalue-to-rvalue conversion (7.1) to x yields a constant expression
(8.6) that does not invoke any non-trivial
functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the
lvalue-to-rvalue conversion (7.1) is applied to e, or e is a
discarded-value expression (8.2).
In particular, the expression ex == a is an element of the set of potential results of the expression e == a->i, according to [basic.def.odr]/2 (2.3), containing the expression ex, where the lvalue-to-rvalue conversion is applied to e.
a is odr-used because you fail the first part of the "unless":
applying the lvalue-to-rvalue conversion (7.1) to x yields a constant expression (8.6) that does not invoke any non-trivial functions
Applying the lvalue-to-rvalue conversion to a does not yield a constant expression.
The rest is core issues 315 and 232.
Your analysis is broken in two additional ways:
The "object expression" is defined using the . form of class member access, so you need to rewrite a->i to dot form, i.e., (*a).i, before applying [basic.def.odr]/2.3. a is not a member of the set of potential results of that expression.
That bullet itself is defective because it was written with non-static data members in mind. For static data members, the set of potential results should be in fact the named static data member - see core issue 2353, so a is doubly not a member of the set of potential results of that expression.
[expr.const]/2.7:
An expression e is a core constant expression unless the
evaluation of e, following the rules of the abstract machine, would
evaluate one of the following expressions:
[...]
an lvalue-to-rvalue conversion unless it is applied to
a non-volatile glvalue of integral or enumeration type that refers to a complete non-volatile const object with a preceding
initialization, initialized with a constant expression, or
a non-volatile glvalue that refers to a subobject of a string literal, or
a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable subobject of
such an object, or
a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e;
[...]
i is a static member of the class... as you can access the static members of the class through using the conventional methods for the instances, they are not tied to any instance in particular, so you don't need to dereference the nullptr pointer (as when you use the sizeof operator). You could also access the field using a simple
return A::i;
statement, because you don't need to create an instance to access it. Indeed, being const, the compiler allows to manage it as a constant value, so only in the case you need to use it's address (by means of the & operator) the compiler can bypass allocating it in read-only memory.
The following sample will probe that:
#include <iostream>
struct A {
static const int i = 1;
};
int main()
{
std::cout << ((A*)0)->i << std::endl;
std::cout << A::i << std::endl;
}
will print
$ a.out
1
1
$ _
Consider this example from cppreference:
struct S { static const int x = 1; };
void f() { &S::x; } // discarded-value expression does not odr-use S::x
I agree that &S::x is a discarded-value expression, since the standard says (9.2, paragraph 1 [stmt.expr] from n4700)
Expression statements have the form
expression-statement:
expression_opt ;
The expression is a discarded-value expression (Clause 8)...
However, is that enough for S::x to not be odr-used? 6.2, paragraph 3 [basic.def.odr] states
A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless
...
if x is an object, ex is an element of the set of potential results of an expression e, where either
the lvalue-to-rvalue conversion (7.1) is applied to e, or
e is a discarded-value expression (Clause 8).
The problem is that the discarded-value expression &S::x has no potential results (which means that S::x is not a potential result of &S::x), as you can see from 6.2, paragraph 2 [basic.def.odr]:
... The set of potential results of an expression e is defined as follows:
If e is an id-expression (8.1.4), the set contains only e.
If e is a subscripting operation (8.2.1) with an array operand, the set contains the potential results of that operand.
...
Otherwise, the set is empty.
Then, how can you explain that S::x is not odr-used?
It is indeed odr-used. Your analysis is correct (and I fixed that example a while ago).
Yes, in the example, &S::x odr-uses S::x.
[basic.def.odr]/4
A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion to x yields a constant expression that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion is applied to e, or e is a discarded-value expression.
The address of an object is never a constant expression. That's why S::x is odr-used in &S::x.
To justify that last assertion:
[expr.const]/6
A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints [...]
and
[expr.const]/2.7
2) An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions:
[...]
2.7) an lvalue-to-rvalue conversion unless it is applied to
(none of the following points applies:)
2.7.1) a non-volatile glvalue of integral or enumeration type that refers to a complete non-volatile const object with a preceding initialization, initialized with a constant expression, or
2.7.2)
a non-volatile glvalue that refers to a subobject of a string literal, or
2.7.3)
a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable subobject of such an object, or
2.7.4)
a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e;
When declaring const int, it may be entirely discarded by the compiler unless you using its address. taking the address is not enough.
Discarded is not mean the value is not evaluated, it is, but it mean there is no memory address containing the const value, the compiler just replace the const variable by its value, as it was just a macro.
In addition, when taking a pointer to it and taking back the value from the pointer, doesn't impress the compiler much, it just ignores it and use the value.
The following code shows it, this code can be compiled and run (I test it by several compilers, I'm still not sure if it successfully compiled by all...) despite of the fact that S::x was not declared:
#include <iostream>
using namespace std;
struct S
{
static const int x=0;
};
//const int S::x; //discarded
int main()
{
const int *px = &S::x; //taking the address
cout<< *px <<endl; //print the value - OK
return 0;
}
But if I'll try to use the address itself(not the value) like:
cout<< px <<endl; //print the address - Will be failed
the link will failed dou to: "undefined reference to S::x".
Therefore, my conclusion is: taking an address without using it, doesn't count at all.
I always thought that a reference would be subjected to an lvalue-to-rvalue-conversion, as any other glvalue, when used in an expression. Nevertheless, it seems like, every time a reference is used in an expression, it is handled in bullet point (2.9) of [expr.const]/2 instead of bullet point (2.7) (in C++14, or C++1z).
Take for example the reference r below, used to initialize variable j. Is it subjected to an lvalue-to-rvalue-conversion?
const int i = 1;
constexpr int& r = i
constexpr int j = r;
According to this answer the reference r is handled in bullet point (2.9) of [expr.const]/2 and not in bullet point 2.7, as I would expect. Why is this?
In some contexts the lvalue-to-rvalue conversion occurs because it is explicitly specified to occur in a given context (for example, for the ternary conditional operator, see here). But it is listed in clause 4 so it is an implicit standard conversion; like all other implicit standard conversions, it occurs when needed. For example, a glvalue of type int will be implicitly converted to a prvalue when used as the operand of an arithmetic expression since its stored value is required.
In the case of constexpr int j = r, yes, the glvalue expression r undergoes lvalue-to-rvalue conversion, since this initialization requires the stored value. Although it isn't explicitly specified that reading the stored value of an object invokes an lvalue-to-rvalue conversion, this fact must obviously be true in the context of the entire standard, as well as the C standard, where the term "rvalue" is not used, but instead the analogous concept of the lvalue conversion refers to the conversion of an lvalue into "the value stored in the designated object".
I'm trying to understand the constant expression concept (from c++reference):
struct S {
static const int c;
};
const int d = 10 * S::c; // not a constant expression: S::c has no preceding
// initializer, this initialization happens after const
const int S::c = 5; // constant initialization, guaranteed to happen first
Why isn't the S::c a constant expression untill we define it. It was declared as a static const data member though...
Quoting relevant part of the C++11 standard (draft N3337), section 5.19, paragraph 2:
A conditional-expression is a core constant expression unless it involves one of the following as a potentially evaluated subexpression (3.2), but subexpressions of logical AND (5.14), logical OR (5.15), and conditional (5.16) operations that are not evaluated are not considered [ Note: An overloaded operator invokes a function. — end note ]:
an lvalue-to-rvalue conversion (4.1) unless it is applied to
a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression
There is no preceding initialization of S::c in your definition of d.
Edit: why this applies:
5.1.1/8: S::c is an lvalue.
3.10/1: a glvalue is an lvalue or xvalue.
5/8: specifies that lvalue-to-rvalue conversion happens whenever an operator that expects a prvalue is used.
Proving that multiplication expects a prvalue is hurting my head. It is implied in many places, but I haven't found anywhere it is said explicitly.
In this sequence …
constexpr int d = 10 * S::c;
const int S::c = 5;
… the value of S::c is not known yet when the d value is compiled. But try to swap these lines:
const int S::c = 5;
constexpr int d = 10 * S::c;
Constant initialization is performed before other initialization in the C++ compiling process. In the example, the constant initialization of d is guaranteed to occur before the constant initialization of S::c. A constant expression must consist exclusively of constant values. When d is initialized, S::c is known to be a constant value, so the expression is not considered constant.