Computation
3.3 Expressions
just by writing a lot of statements. Why do we mention this now? At this stage you (or at least many readers) have little idea about what code is, and it will be months before you are ready to write code upon which other people could depend for their lives or livelihood. We mention it to help you get the emphasis of your learning right. It is very tempting to dash ahead, focusing on the parts of programming that – like what is described in the rest of this chapter – are concrete and immediately useful and to ignore the “softer,”
more conceptual parts of the art of software development. However, good programmers and system designers know (often having learned it the hard way) that concerns about structure lie at the heart of good software and that ignoring structure leads to expensive messes.
Without structure, you are (metaphorically speaking) building with mud bricks. It can be done, but you’ll never get to the tenth floor (classical mud bricks lack the structural strength for that). If you have the ambition to build something reasonably permanent or something large, you pay attention to matters of code structure and organization along the way, rather than having to come back and learn them after failures.
Click here to view code image
length = 99; // assign 99 to length
Here, as the left-hand operand of the assignment, length means “the object named length,” so that the assignment expression is read “Put 99 into the object named length.” We distinguish between length used on the left-hand side of an assignment or an initialization (the lvalue of length: “the object named length”) and length used on the right-hand side of an assignment or initialization (the rvalue of length: “the value of the object named length,” or just “the value of length”). In this context, we find it useful to visualize a variable as a box labeled by its name (§2.2, §2.5):
That is, length is the name of an object of type int containing the value 99. Sometimes (as an lvalue) length refers to the box (object) and sometimes (as an rvalue) length refers to the value in that box.
We can make more complicated expressions by using operators, such as +
and *, in just the way that we are used to. When needed, we can use parentheses to group expressions:
Click here to view code image
int perimeter = (length+width)*2; // add then multiply
Without parentheses, we’d have had to say Click here to view code image
int perimeter = length*2+width*2;
which is clumsy, and we might even have made this mistake:
Click here to view code image
int perimeter = length+width*2; // add width*2 to length
This last error is logical and cannot be found by the compiler. All the
compiler sees is a variable called perimeter initialized by a valid expression. If the result of that expression is nonsense, that’s your problem. You know the
mathematical definition of a perimeter, but the compiler doesn’t.
The usual mathematical rules of operator precedence apply, so
length+width*2 means length+(width*2). Similarly a*b+c/d means (a*b)+(c/d) and not a*(b+c)/d. See cppreference (§0.4.1) for an operator precedence table.
The first rule for the use of parentheses is simply “If in doubt,
parenthesize,” but please do learn enough about expressions so that you are not in doubt about a*b+c/d. Overuse of parentheses, as in (a*b)+(c/d), decreases readability.
Why should you care about readability? Because you and possibly others will read your code. Ugly code slows down reading and comprehension. Ugly code is not just hard to read, it is also much harder to get correct. Ugly code often hides logical errors. Don’t write absurdly complicated expressions such as
Click here to view code image
a*b+c/d*(e/-f/g)/h+7 // too complicated
and always try to choose meaningful names.
3.3.1 Constant expressions
Programs typically use a lot of constants. For example, a geometry program might use pi and an inch-to-centimeter conversion program will use a
conversion factor such as 2.54. Obviously, we want to use meaningful names for those constants (as we did for pi; we didn’t say 3.14159). Similarly, we don’t want to change those constants accidentally. Consequently, C++ offers the notion of a symbolic constant, that is, a named object to which you can’t give a new value after it has been initialized. For example:
Click here to view code image
constexpr double pi = 3.14159;
pi = 7; // error: assignment to constant
double c = 2*pi*r; // OK: we just read pi; we don’t try to change it
Such constants are useful for keeping code readable. You might have
recognized 3.14159 as an approximation to pi if you saw it in some code, but would you have recognized 299792458? Also, if someone asked you to change some code to use pi with the precision of 12 digits for your computation, you
could search for 3.14 in your code, but if someone incautiously had used 22/7, you probably wouldn’t find it. It would be much better just to change the definition of pi to use the more appropriate value:
Click here to view code image
constexpr double pi = 3.14159265359;
AA
Consequently, we prefer not to use literals (except very obvious ones, such as
0 and 1) in most places in our code. Instead, we use constants with descriptive names. Non-obvious literals in code (outside definitions of symbolic
constants) are derisively referred to as magic constant. And by the way,
299792458 is one of the fundamental constants of the universe: the speed of light in vacuum measured in meters per second. If you didn’t instantly recognize that, why would you expect not to be confused and slowed down by other literals embedded in code? Avoid magic constants!
XX
A constexpr symbolic constant must be given a value that is known at compile time (a constant expression). For example:
Click here to view code image
constexpr int max = 100;
int n;
cin >> n;
constexpr int c1 = max+7; // OK: c1 is 107
constexpr int c2 = n+7; // error: we don’t know the value of n
To handle cases where the value of a constant that is initialized with a value that is not known at compile time but never changes after initialization, C++
offers a second form of constant (a const):
Click here to view code image
int n;
cin >> n;
const int c3 = n; // OK
c3 = 7; // error: c3 is a const
Such “const variables” are very common for two reasons:
C++98 did not have constexpr, so people used const.
“Variables” that are not constant expressions (their value is not known at compile time) but do not change values after initialization are in
themselves widely useful.
3.3.2 Operators
We just used the simplest operators. However, you will soon need more as you want to express more complex operations. Most operators are
conventional, so we’ll just explain them later as needed and you can look up details if and when you find a need. Here are some common operators:
Example Name Comment
lval=a assignment not to be confused with ==
++lval pre-increment increment and use the incremented value
−−lval pre-decrement decrement and use the decremented value
!a not result is bool
−a unary minus
a*b multiply
a/b divide
a%b modulo
(remainder)
only for integer types
a+b add
a−b subtract
out<<b write b to out where out is an ostream in>>b read from in into
b
where in is an istream
lval*=a compound
assignment
lval = lval*a; also for /, %, +, −
f(a) function call pass a to f as an argument (§3.5)
f<T>(a) function template call
pass a to f<T> as an argument (§21.2)
[](a){S} lambda expression
create a function object taking a as an argument (§21.2.3)
We used lval (short for lvalue, that is a value that can appear on the left-hand side of an assignment) where the operator modifies an operand.
Example Name Comment
a<b less than result is bool
a<=b less than or equal result is bool
a>b greater than result is bool
a>=b greater than or equal result is bool
a==b equal not to be confused with = a!=b not equal result is bool
a&&b logical and result is bool a||b logical or result is bool
T{a} widening conversion result is the value of a converted to type T
See cppreference (§0.4.1) for a complete list of operators. For examples of the use of the logical operators && (and), || (or), and ! (not), see §4.5.1, §6.7, and §9.3.1.
XX
Note that a<b<c means (a<b)<c and that a<b evaluates to a Boolean value:
true or false. So, a<b<c will be equivalent to either true<c or false<c. In
particular, a<b<c does not mean “Is b between a and c?” as many have naively (and not unreasonably) assumed. Thus, a<b<c is basically a useless
expression. Don’t write such expressions with two comparison operations,
and be very suspicious if you find such an expression in someone else’s code – it is most likely an error.
An increment can be expressed in at least three ways:
++a a+=1 a=a+1
AA
Which notation should we use? Why? We prefer the first version, ++a,
because it more directly expresses the idea of incrementing. It says what we want to do (increment a) rather than how to do it (add 1 to a and then write the result to a). In general, a way of saying something in a program is better than another if it more directly expresses an idea. The result is more concise and easier for a reader to understand. If we wrote a=a+1, a reader could easily wonder whether we really meant to increment by 1. Maybe we just mistyped
a=b+1, a=a+2, or even a=a−1; with ++a there are far fewer opportunities for such doubts. Please note that this is a logical argument about readability and
correctness, not an argument about efficiency. Contrary to popular belief, modern compilers tend to generate exactly the same code from a=a+1 as for
++a when a is one of the built-in types. Similarly, we prefer a*=scale over
a=a*scale.