B. Stroustrup - The C++ Programming Language (794319), страница 60
Текст из файла (страница 60)
Compilers can warn about such ambiguities. Unfortunately, most do not, so be careful not towrite an expression that reads or writes an object more than once, unless it does so using a single260ExpressionsChapter 10operator that makes it well defined, such as ++ and +=, or explicitly express sequencing using ,(comma), &&, or ||.The operators , (comma), && (logical and), and || (logical or) guarantee that their left-hand operand is evaluated before their right-hand operand. For example, b=(a=2,a+1) assigns 3 to b.
Examples of the use of || and && can be found in §10.3.3. For built-in types, the second operand of && isevaluated only if its first operand is true, and the second operand of || is evaluated only if its first operand is false; this is sometimes called short-circuit evaluation. Note that the sequencing operator ,(comma) is logically different from the comma used to separate arguments in a function call. Forexample:f1(v[i],i++);f2( (v[i],i++) );// two arguments// one argumentThe call of f1 has two arguments, v[i] and i++, and the order of evaluation of the argument expressions is undefined.
So it should be avoided. Order dependence of argument expressions is verypoor style and has undefined behavior. The call of f2 has only one argument, the comma expression(v[i],i++), which is equivalent to i++. That is confusing, so that too should be avoided.Parentheses can be used to force grouping.
For example, a∗b/c means (a∗b)/c, so parenthesesmust be used to get a∗(b/c); a∗(b/c) may be evaluated as (a∗b)/c only if the user cannot tell the difference. In particular, for many floating-point computations a∗(b/c) and (a∗b)/c are significantly different, so a compiler will evaluate such expressions exactly as written.10.3.3 Operator PrecedencePrecedence levels and associativity rules reflect the most common usage. For example:if (i<=0 || max<i) // ...means ‘‘if i is less than or equal to 0 or if max is less than i.’’ That is, it is equivalent toif ( (i<=0) || (max<i) ) // ...and not the legal but nonsensicalif (i <= (0||max) < i) // ...However, parentheses should be used whenever a programmer is in doubt about those rules.
Use ofparentheses becomes more common as the subexpressions become more complicated, but complicated subexpressions are a source of errors. Therefore, if you start feeling the need for parentheses,you might consider breaking up the expression by using an extra variable.There are cases when the operator precedence does not result in the ‘‘obvious’’ interpretation.For example:if (i&mask == 0)// oops! == expression as operand for &This does not apply a mask to i and then test if the result is zero.
Because == has higher precedencethan &, the expression is interpreted as i&(mask==0). Fortunately, it is easy enough for a compiler towarn about most such mistakes. In this case, parentheses are important:if ((i&mask) == 0) // ...It is worth noting that the following does not work the way a mathematician might expect:Section 10.3.3Operator Precedence261if (0 <= x <= 99) // ...This is legal, but it is interpreted as (0<=x)<=99, where the result of the first comparison is either trueor false. This Boolean value is then implicitly converted to 1 or 0, which is then compared to 99,yielding true. To test whether x is in the range 0..99, we might useif (0<=x && x<=99) // ...A common mistake for novices is to use = (assignment) instead of == (equals) in a condition:if (a = 7) // oops! constant assignment in conditionThis is natural because = means ‘‘equals’’ in many languages.
Again, it is easy for a compiler towarn about most such mistakes – and many do. I do not recommend warping your style to compensate for compilers with weak warnings. In particular, I don’t consider this style worthwhile:if (7 == a) // try to protect against misuse of =; not recommended10.3.4 Temporary ObjectsOften, the compiler must introduce an object to hold an intermediate result of an expression. Forexample, for v=x+y∗z the result of y∗z has to be put somewhere before it is added to x.
For built-intypes, this is all handled so that a temporary object (often referred to as just a temporary) is invisible to the user. However, for a user-defined type that holds a resource knowing the lifetime of atemporary can be important. Unless bound to a reference or used to initialize a named object, atemporary object is destroyed at the end of the full expression in which it was created.
A fullexpression is an expression that is not a subexpression of some other expression.The standard-library string has a member c_str() (§36.3) that returns a C-style pointer to a zeroterminated array of characters (§2.2.5, §43.4). Also, the operator + is defined to mean string concatenation. These are useful facilities for strings. However, in combination they can cause obscureproblems. For example:void f(string& s1, string& s2, string& s3){const char∗ cs = (s1+s2).c_str();cout << cs;if (strlen(cs=(s2+s3).c_str())<8 && cs[0]=='a') {// cs used here}}Probably, your first reaction is ‘‘But don’t do that!’’ and I agree.
However, such code does get written, so it is worth knowing how it is interpreted.A temporary string object is created to hold s1+s2. Next, a pointer to a C-style string isextracted from that object. Then – at the end of the expression – the temporary object is deleted.However, the C-style string returned by c_str() was allocated as part of the temporary object holdings1+s2, and that storage is not guaranteed to exist after that temporary is destroyed. Consequently,cs points to deallocated storage. The output operation cout<<cs might work as expected, but thatwould be sheer luck.
A compiler can detect and warn against many variants of this problem.262ExpressionsChapter 10The problem with the if-statement is a bit more subtle. The condition will work as expectedbecause the full expression in which the temporary holding s2+s3 is created is the condition itself.However, that temporary is destroyed before the controlled statement is entered, so any use of csthere is not guaranteed to work.Please note that in this case, as in many others, the problems with temporaries arose from usinga high-level data type in a low-level way. A cleaner programming style yields a more understandable program fragment and avoids the problems with temporaries completely. For example:void f(string& s1, string& s2, string& s3){cout << s1+s2;string s = s2+s3;if (s.length()<8 && s[0]=='a') {// use s here}}A temporary can be used as an initializer for a const reference or a named object.
For example:void g(const string&, const string&);void h(string& s1, string& s2){const string& s = s1+s2;string ss = s1+s2;g(s,ss);// we can use s and ss here}This is fine. The temporary is destroyed when ‘‘its’’ reference or named object goes out of scope.Remember that returning a reference to a local variable is an error (§12.1.4) and that a temporaryobject cannot be bound to a non-const lvalue reference (§7.7).A temporary object can also be created explicitly in an expression by invoking a constructor(§11.5.1). For example:void f(Shape& s, int n, char ch){s.move(string{n,ch});// construct a string with n copies of ch to pass to Shape::move()// ...}Such temporaries are destroyed in exactly the same way as the implicitly generated temporaries.10.4 Constant ExpressionsC++ offers two related meanings of ‘‘constant’’:• constexpr: Evaluate at compile time (§2.2.3).• const: Do not modify in this scope (§2.2.3, §7.5).Basically, constexpr’s role is to enable and ensure compile-time evaluation, whereasconst’sSection 10.4Constant Expressions263primary role is to specify immutability in interfaces.
This section is primarily concerned with thefirst role: compile-time evaluation.A constant expression is an expression that a compiler can evaluate. It cannot use values thatare not known at compile time and it cannot have side effects. Ultimately, a constant expressionmust start out with an integral value (§6.2.1), a floating-point value (§6.2.5), or an enumerator(§8.4), and we can combine those using operators and constexpr functions that in turn produce values. In addition, some addresses can be used in some forms of constant expressions. For simplicity, I discuss those separately in §10.4.5.There are a variety of reasons why someone might want a named constant rather than a literal ora value stored in a variable:[1] Named constants make the code easier to understand and maintain.[2] A variable might be changed (so we have to be more careful in our reasoning than for aconstant).[3] The language requires constant expressions for array sizes, case labels, and templatevalue arguments.[4] Embedded systems programmers like to put immutable data into read-only memorybecause read-only memory is cheaper than dynamic memory (in terms of cost and energyconsumption), and often more plentiful.
Also, data in read-only memory is immune tomost system crashes.[5] If initialization is done at compile time, there can be no data races on that object in amulti-threaded system.[6] Sometimes, evaluating something once (at compile time) gives significantly better performance than doing so a million times at run time.Note that reasons [1], [2], [5], and (partly) [4] are logical. We don’t just use constant expressionsbecause of an obsession with performance. Often, the reason is that a constant expression is a moredirect representation of our system requirements.As part of the definition of a data item (here, I deliberately avoid the word ‘‘variable’’),constexpr expresses the need for compile-time evaluation.
If the initializer for a constexpr can’t beevaluated at compile time, the compiler will give an error. For example:int x1 = 7;constexpr int x2 = 7;constexpr int x3 = x1;constexpr int x4 = x2;// error : initializer is not a constant expression// OKvoid f(){constexpr int y3 = x1;constexpr int y4 = x2;// ...}// error : initializer is not a constant expression// OKA clever compiler could deduce that the value of x1 in the initializer for x3 was 7.
However, weprefer not to rely on degrees of cleverness in compilers. In a large program, determining the valuesof variables at compile time is typically either very difficult or impossible.264ExpressionsChapter 10The expressive power of constant expressions is great. We can use integer, floating-point, andenumeration values. We can use any operator that doesn’t modify state (e.g., +, ?:, and [], but not =or ++). We can use constexpr functions (§12.1.6) and literal types (§10.4.3) to provide a significantlevel of type safety and expressive power. It is almost unfair to compare this to what is commonlydone with macros (§12.6).The conditional-expression operator ?: is the means of selection in a constant expression.