Expressions in C++

Introduction

Expressions in C++ are fundamental constructs made up of operators, constants, and variables, following the language’s syntactical rules. Every expression is a segment of a code that returns a value. For instance:

This example demonstrates the creation of variables to store values: a box for $x$ and another for $y$, where $y$ equals the expression $x + 13$ (thus, $y = 23$). Now, let’s delve into a more complex example:

This statement encompasses three expressions:

* The results of the expression $3 - x$ is stored in the variable $y$ * The expression $y = 3 - x$ returns the value of $y$, and it is stored in the variable $v$ * The results of the expression $y \times \left(\frac{v}{5} + x\right)$ is stored in the variable $z$

It’s essential to remember the precedence of operations: multiplication and division are executed before addition and subtraction. For example:

1-3*4 = -11
2/3-4*2/3 = -2
2/3-4/4*2/3 = 0

Operator precedence in C++ determines the sequence of operations in an expression. Operators have a specific order of execution relative to others. For instance, in the expression $\frac{2}{4} - 3 + 4 \times 6$, the subexpressions $\frac{2}{4}$and$4 \times 6$ are calculated first, followed by the addition and subtraction. When operators have the same precedence, their associativity dictates the order - either left-to-right or right-to-left.

Precedence order

Precedence order

Associativity specifies the order of operations for operators with the same precedence level. It can be left-to-right or right-to-left. Typically, addition, subtraction, multiplication, and division are left-associative, while assignment operators are right-associative. Some operators are non-associative, meaning their behaviour is undefined if used sequentially in an expression. Parentheses can alter the default associativity, enforcing a specific order.

Example of left-associative, right-associative, and non-associative

Example of left-associative, right-associative, and non-associative

Using Parentheses ()

The operator () has the highest precedente order (see Table 1), as consequence, we can use parentheses to change the sequence of operators. Consider the following example:

5 + 6 * 7

The * operator is evaluated firstly, followed by the + operator, so the result is $5+6\times 7 = 47$. However, if we want to account for the addiction first and then the multiplication, we can rewrite the code as:

(5 + 6) * 7

Then, the program will compute $\left(5+6\right)\times 7=11\times 7=77$. Sometimes, parentheses' inclusion should be important to make your code easier to understand, and therefore easier to maintain.

Modulus operator (%)

The modulus operator evaluates the remainder when dividing the first operand by the second one. Ex.: a % b is the remainder when $a$ is divided

by $b$ ($a$ modulus $b$).

by $b$ ($a$ modulus $b$).

Example of modulus

Example of modulus

* Dividing an integer by another one gives an integer.

Example:

int x = 10;
int y = 3;

x/y = 10/3 = 3 (dividing two integers)

x % y = 1 (modulus)

Short hand or syntatic sugar

Short hand expressions provide a straightforward way to write common patterns over the algorithm for initialized variables.

Short hand Meaning Prefix and Postfix
$x+=y$ $x=x+y$
$x-=y$ $x=x-y$
$x*=y$ $x= x \times y$
$x/=y$ $x=x/y$
$x++$ $x=x+1$ Return the value of $x$ first then increment it
$++x$ $x=x+1$ Increment first then return the value of $x$
$x–$ $x=x-1$ Return the value of $x$ first then increment it
$–x$ $x=x-1$ Increment first then return the value of $x$

Example 1:

Here you can see that y ++= x * z; is calculate as $y=y+x \times z = 30 + 2 \times 4 = 38$.

Example 2:

In this example you can see that we used the postfix x++ to first initialize $y$ ($y=8 \times x = 8 \times 7 = 56$) and then update $x$ to x=x+1=8. On the other hand, we used the prefix --y to first update the variable $y$ to y=y-1=55 and then calculate the variable z using the updated $y$ $\left(z = y/5 = 55/5 = 11 \right)$.

Note that when we use x*= (y/z) % 2 the variable $x$ multiply the entire expression after = symbol. This expression is equivalent to x = x * ((y/z) % 2));.

Operator precedence and associativity

Table 1 shows a list of precedence (ordered) and associativity of C operators. This table was obtained from cppreference.com.

Table 1: Precedence and associativity of C operators
Precedence Operator Description Associativity
1 ++ \-\- Suffix/postfix increment and decrement Left-to-right
() Function call
[] Array subscripting
. Structure and union member access
-> Structure and union member access through pointer
(type){list} Compound literal(C99)
2 ++ \-\- Prefix increment and decrement[note 1] Right-to-left
+ - Unary plus and minus
! ~ Logical NOT and bitwise NOT
(type) Cast
* Indirection (dereference)
& Address-of
sizeof Size-of[note 2]
_Alignof Alignment requirement(C11)
3 * / % Multiplication, division, and remainder Left-to-right
4 + - Addition and subtraction
5 << >> Bitwise left shift and right shift
6 < <= For relational operators < and ≤ respectively
> >= For relational operators > and ≥ respectively
7 == != For relational = and ≠ respectively
8 & Bitwise AND
9 ^ Bitwise XOR (exclusive or)
10 | Bitwise OR (inclusive or)
11 && Logical AND
12 || Logical OR
13 ?: Ternary conditional[note 3] Right-to-Left
14[note 4] = Simple assignment
+= -= Assignment by sum and difference
*= /= %= Assignment by product, quotient, and remainder
<<= >>= Assignment by bitwise left shift and right shift
&= ^= |= Assignment by bitwise AND, XOR, and OR
15 , Comma Left-to-right
  1. The operand of prefix ++ and \-\- can't be a type cast. This rule grammatically forbids some expressions that would be semantically invalid anyway. Some compilers ignore this rule and detect the invalidity semantically.
  2. The operand of sizeof can't be a type cast: the expression sizeof (int) * p is unambiguously interpreted as (sizeof(int)) * p, but not sizeof((int)*p).
  3. The expression in the middle of the conditional operator (between ? and :) is parsed as if parenthesized: its precedence relative to ?: is ignored.
  4. Assignment operators' left operands must be unary (level-2 non-cast) expressions. This rule grammatically forbids some expressions that would be semantically invalid anyway. Many compilers ignore this rule and detect the invalidity semantically. For example, e = a < d ? a++ : a = d is an expression that cannot be parsed because of this rule. However, many compilers ignore this rule and parse it as e = ( ((a < d) ? (a++) : a) = d ), and then give an error because it is semantically invalid.

Impact of Data Types on Expressions

In C++, the data type of the variables involved in an expression significantly impacts the result. For instance, dividing two integers results in an integer, while using at least one floating-point number yields a floating-point result. Understanding how data types interact within expressions is crucial for accurate calculations and avoiding common pitfalls like integer truncation.

Here are some key points about integer truncation and other common pitfalls in C++:

  1. Integer Truncation: This occurs when the result of a division or other operation between integers is a floating-point number, but the data type is an integer. For example, int result = 5 / 2; will store 2 in result, not 2.5, as the fractional part is truncated.

  2. Implicit Type Conversions: C++ automatically converts types in certain situations, which can lead to unexpected results. For instance, mixing signed and unsigned integers in expressions can cause unexpected behaviours due to implicit type conversions.

  3. Overflow and Underflow: This happens when a variable is assigned a value outside its range. For example, storing a value larger than the maximum value that an int can hold will result in overflow, leading to unexpected values.

  4. Precision Loss in Floating-Point Numbers: Floating-point variables can lose precision, especially when dealing with very large or very small numbers. This can result in rounding errors in calculations.

  5. Division by Zero: This can occur if a program inadvertently divides a number by zero. It’s a critical error in C++ and can cause a program to crash or behave unpredictably.

  6. Uninitialized Variables: Using variables before initializing them can lead to unpredictable results, as they may contain random data.

  7. Pointer Errors: Common mistakes with pointers include dereferencing a null or uninitialized pointer, pointer arithmetic errors, and memory leaks.

  8. Operator Precedence Mistakes: Misunderstanding the order in which operations are performed can lead to bugs. For example, assuming that a + b * c adds a and b before multiplying by c (it doesn’t; multiplication is done first).

  9. Assuming Size of Data Types is Constant: The size of data types like int can vary depending on the system. Assuming a constant size can lead to errors, particularly when performing operations like bit manipulation or working with binary file formats.

  10. Not Checking the Return Value of Functions: When functions return values to indicate success or failure, not checking these can lead to the program continuing as if nothing went wrong, even when errors have occurred.

Role of Type Casting in Expressions

Type casting in expressions can be used to explicitly convert data from one type to another. This technique is particularly useful in situations where operations between different data types are necessary. For example, casting an integer to a float in a division operation to obtain a floating-point result. However, it’s important to use type casting judiciously to maintain the precision and integrity of data.

The Significance of Expression Evaluation Order

While operator precedence and associativity rules dictate the order of operations in an expression, the sequence in which expressions are evaluated can also be influenced by function calls, side effects, and sequence points. Understanding how C++ evaluates expressions, especially in complex statements, is essential for debugging and writing predictable code.

Compiler Optimizations and Expressions

Modern C++ compilers often optimize expressions to enhance performance. These optimizations might include reordering operations (while respecting the language rules), eliminating redundant calculations, or simplifying expressions at compile time. Being aware of these potential optimizations can help in writing more efficient code and understanding any discrepancies between the written code and its execution behaviour.

Best Practices for Writing Expressions

To maintain readability and reduce errors in C++, it’s advisable to write clear and simple expressions. Avoid overly complex expressions, use parentheses to clarify order of operations, and follow coding standards and guidelines. Readable expressions are easier to debug, maintain, and understand, especially in collaborative environments.

Adding these paragraphs can provide a more comprehensive and nuanced understanding of expressions in C++, catering to both beginners and experienced programmers.

References

Citation

  1. For attribution, please cite this work as:
Oliveira T.P. (2020, Dec. 16). Expressions in C++
  1. BibTeX citation
@misc{oliveira2020expression,
  author = {Oliveira, Thiago},
  title = {Expressions in C++},
  url = {https://prof-thiagooliveira.netlify.app/post/expressions/},
  year = {2020}
}

Did you find this page helpful? Consider sharing it 🙌

Thiago de Paula Oliveira
Thiago de Paula Oliveira
Consultant Statistician

My research interests include statistical modelling, agriculture, genetics, and sports.

Related