Book Image

Modern C++: Efficient and Scalable Application Development

By : Richard Grimes, Marius Bancila
Book Image

Modern C++: Efficient and Scalable Application Development

By: Richard Grimes, Marius Bancila

Overview of this book

C++ is one of the most widely used programming languages. It is fast, flexible, and used to solve many programming problems. This Learning Path gives you an in-depth and hands-on experience of working with C++, using the latest recipes and understanding most recent developments. You will explore C++ programming constructs by learning about language structures, functions, and classes, which will help you identify the execution flow through code. You will also understand the importance of the C++ standard library as well as memory allocation for writing better and faster programs. Modern C++: Efficient and Scalable Application Development deals with the challenges faced with advanced C++ programming. You will work through advanced topics such as multithreading, networking, concurrency, lambda expressions, and many more recipes. By the end of this Learning Path, you will have all the skills to become a master C++ programmer. This Learning Path includes content from the following Packt products: • Beginning C++ Programming by Richard Grimes • Modern C++ Programming Cookbook by Marius Bancila • The Modern C++ Challenge by Marius Bancila
Table of Contents (24 chapters)
Title Page
Copyright
About Packt
Contributors
Preface
12
Math Problems
13
Language Features
14
Strings and Regular Expressions
15
Streams and Filesystems
16
Date and Time
17
Algorithms and Data Structures
Index

Using Operators


Operators are used to compute a value from one or more operands. The following table groups all of the operators with equal precedence and lists their associativity. The higher in the table, the higher precedence of execution the operator has in an expression. If you have several operators in an expression, the compiler will perform the higher-precedence operators before the lower-precedence operators. If an expression contains operators of equal precedence, then the compiler will use the associativity to decide whether an operand is grouped with the operator to its left or right.

Note

There are some ambiguities in this table. A pair of parentheses can mean a function call or a cast and in the table these are listed as function() and cast(); in your code you will simply use (). The + and - symbols are either used to indicate sign (unary plus and unary minus, given in the table as +x and -x), or addition and subtraction (given in the table as + and -). The & symbol means either "take the address of" (listed in the table as &x) or bitwise AND (listed in the table as &). Finally, the postfix increment and decrement operators (listed in the table as x++ and x--) have a higher precedence than the prefix equivalents (listed as ++x and --x).

Precedence and Associativity

Operators

1: No associativity

::

2: Left to right associativity

. or -> [] function() {} x++ x-- typeid const_cast dynamic_cast reinterpret_cast static_cast

3: Right to left associativity

sizeof ++x --x ~ ! -x +x &x * new delete cast()

4: Left to right associativity

.* or ->*

5: Left to right associativity

* / %

6: Left to right associativity

+ -

7: Left to right associativity

<< >>

8: Left to right associativity

< > <= >=

9: Left to right associativity

== !=

10: Left to right associativity

&

11: Left to right associativity

^

12: Left to right associativity

|

13: Left to right associativity

&&

14: Left to right associativity

||

15: Right to left associativity

? :

16: Right to left associativity

= *= /= %= += -= <<= >>= &= |= ^=

17: Right to left associativity

throw

18: Left to right associativity

,

 

For example, take a look at the following code:

    int a = b + c * d;

This is interpreted as the multiplication being performed first, and then the addition. A clearer way to write the same code is:

    int a = b + (c * d);

The reason is that * has a higher precedence than + so that the multiplication is carried out first, and then the addition is performed:

    int a = b + c + d;

In this case, the + operators have the same precedence, which is higher than the precedence of assignment. Since + has left to right associativity the statement is interpreted as follows:

    int a = ((b + c) + d);

That is, the first action is the addition of b and c, and the result is added to d and it is this result that is used to assign a. This may not seem important, but bear in mind that the addition could be between function calls (a function call has a higher precedence than +):

    int a = b() + c() + d();

This means that the three functions are called in the order b, c, d, and then their return values are summed according to the left-to-right associativity. This may be important because d may depend on global data altered by the other two functions.

It makes your code more readable and easier to understand if you explicitly specify the precedence by grouping expressions with parentheses. Writing b + (c * d) makes it immediately clear which expression is executed first, whereas b + c * d means you have to know the precedence of each operator.

The built-in operators are overloaded, that is, the same syntax is used regardless of which built-in type is used for the operands. The operands must be the same type; if different types are used, the compiler will perform some default conversions, but in other cases (in particular, when operating on types of different sizes), you will have to perform a cast to indicate explicitly what you mean. 

Exploring the Built-in Operators

C++ comes with a wide range of built-in operators; most are arithmetic or logic operators, which will be covered in this section. The memory operators will be covered in Chapter 2, Working with Memory, Arrays, and Pointers, and the object-related operators in Chapter 4, Classes.

Arithmetic Operators

The arithmetic operators +, -, /, *, and % need little explanation other than perhaps the division and modulus operators. All of these operators act upon integer and real numeric types except for %, which can only be used with integer types. If you mix the types (say, add an integer to a floating-point number) then the compiler will perform an automatic conversion. The division operator / behaves as you expect for floating point variables: it produces the result of the division of the two operands. When you perform the division between two integers a / b, the result is the whole number of the divisor (b) in the dividend (a). The remainder of the division is obtained by the modulus %. So, for any integer, b (other than zero), one could say that, an integer a can be expressed as follows:

    (a / b) * b + (a % b)

Note that the modulus operator can only be used with integers. If you want to get the remainder of a floating-point division, use the standard function, std:;remainder.

Be careful when using division with integers, since fractional parts are discarded. If you need the fractional parts, then you may need to explicitly convert the numbers into real numbers. For example:

    int height = 480; 
    int width = 640; 
    float aspect_ratio = width / height;

This gives an aspect ratio of 1 when it should be 1.3333 (or 4 : 3). To ensure that floating-point division is performed, rather than integer division, you can cast either (or both) the dividend or divisor to a floating-point number.

Increment and Decrement Operators

There are two versions of these operators, prefix and postfix. As the name suggests, prefix means that the operator is placed on the left of the operand (for example, ++i), and a postfix operator is placed to the right (i++). The ++ operator will increment the operand and the -- operator will decrement it. The prefix operator means "return the value after the operation," and the postfix operator means "return the value before the operation." So the following code will increment one variable and use it to assign another:

    a = ++b;

Here, the prefix operator is used so the variable b is incremented and the variable a is assigned to the value after b has been incremented. Another way of expressing this is:

    a = (b = b + 1);

The following code assigns a value using the postfix operator:

    a = b++;

This means that the variable b is incremented, but the variable a is assigned to the value before b has been incremented. Another way of expressing this is:

    int t; 
    a = (t = b, b = b + 1, t);

Note

Note that this statement uses the comma operator, so a is assigned to the temporary variable t in the right-most expression.

The increment and decrement operators can be applied to both integer and floating point numbers. The operators can also be applied to pointers, where they have a special meaning. When you increment a pointer variable it means increment the pointer by the size of the type pointed to by the operator.

Bitwise Operators

Integers can be regarded as a series of bits, 0 or 1. Bitwise operators act upon these bits compared to the bit in the same position in the other operand. Signed integers use a bit to indicate the sign, but bitwise operators act on every bit in an integer, so it is usually only sensible to use them on unsigned integers. In the following, all the types are marked as unsigned, so they are treated as not having a sign bit.

The & operator is bitwise AND, which means that each bit in the left-hand operand is compared with the bit in the right-hand operand in the same position. If both are 1, the resultant bit in the same position will be 1; otherwise, the resultant bit is zero:

    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 
    unsigned int b = 0x00ff; // this is the binary 0000000000001111 
    unsigned int c = a & b;  // this is the binary 0000000000001010 
    std::cout << std::hex << std::showbase << c << std::endl;

In this example, using bitwise & with 0x00ff has the same effect as providing a mask that masks out all but the lowest byte.

The bitwise OR operator | will return a value of 1 if either or both bits in the same position are 1, and a value of 0 only if both are 0:

    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 
    unsigned int b = 0x00ff; // this is the binary 0000000000001111 
    unsigned int c = a & b;  // this is the binary 0000101000001111 
    std::cout << std::hex << std::showbase << c << std::endl;

One use of the & operator is to find if a particular bit (or a specific collection of bits) is set:

    unsigned int flags = 0x0a0a; // 0000101000001010 
    unsigned int test = 0x00ff;  // 0000000000001111 

    // 0000101000001111 is (flags & test) 
    if ((flags & test) == flags)  
    { 
        // code for when all the flags bits are set in test 
    } 
    if ((flags & test) != 0) 
    { 
        // code for when some or all the flag bits are set in test  
    }

The flags variable has the bits we require, and the test variable is a value that we are examining. The value (flags & test) will have only those bits in the test variables that are also set in flags. Thus, if the result is non-zero, it means that at least one bit in test is also set in flags; if the result is exactly the same as the flags variable then all the bits in flags are set in test.

The exclusive OR operator ^ is used to test when the bits are different; the resultant bit is 1 if the bits in the operands are different, and 0 if they are the same. Exclusive OR can be used to flip specific bits:

    int value = 0xf1; 
    int flags = 0x02; 
    int result = value ^ flags; // 0xf3 
    std::cout << std::hex << result << std::endl;

The final bitwise operator is the bitwise complement ~. This operator is applied to a single integer operand and returns a value where every bit is the complement of the corresponding bit in the operand; so if the operand bit is 1, the bit in the result is 0, and if the bit in the operand is 0, the bit in the result is 1. Note that all bits are examined, so you need to be aware of the size of the integer.

Boolean Operators

The == operator tests whether two values are exactly the same. If you test two integers then the test is obvious; for example, if x is 2 and y is 3, then x == y is obviously false. However, two real numbers may not be the same even when you think so:

    double x = 1.000001 * 1000000000000; 
    double y = 1000001000000; 
    if (x == y) std::cout << "numbers are the same";

The double type is a floating-point type held in 8 bytes, but this is not enough for the precision being used here; the value stored in the x variable is 1000000999999.9999 (to four decimal places).

The != operator tests if two values are not true. The operators > and <, test two values to see if the left-hand operand is greater than, or less than, the right-hand operand, the >= operator tests if the left-hand operand is greater than or equal to the right-hand operand, and the <= operator tests if the left-hand operand is less than or equal to the right-hand operand. These operators can be used in the if statement similar to how == is used in the preceding example. The expressions using the operators return a value of type bool and so you can use them to assign values to Boolean variables:

    int x = 10; 
    int y = 11; 
    bool b = (x > y); 
    if (b) std::cout << "numbers same"; 
    else   std::cout << "numbers not same";

The assignment operator (=) has a higher precedence than the greater than (>=) operator, but we have used the parentheses to make it explicit that the value is tested before being used to assign the variable. You can use the ! operator to negate a logical value. So, using the value of b obtained previously, you can write the following:

    if (!b) std::cout << "numbers not same"; 
    else    std::cout << "numbers same";

You can combine two logical expressions using the && (AND) and || (OR) operators. An expression with the && operator is true only if both operands are true, whereas an expression with the || operator is true if either, or both, operands are true:

    int x = 10, y = 10, z = 9; 
    if ((x == y) || (y < z)) 
        std::cout << "one or both are true";

This code involves three tests; the first tests if the x and y variables have the same value, the second tests if the variable y is less than z, and then there is a test to see if either or both of the first two tests are true.

In a || expression such as this, where the first operand (x==y) is true, the total logical expression will be true regardless of the value of the right operand (here, y < z). So there is no point in testing the second expression. Correspondingly, in an && expression, if the first operand is false then the entire expression must be false, and so the right-hand part of the expression need not be tested.

 

The compiler will provide code to perform this short-circuiting for you:

    if ((x != 0) && (0.5 > 1/x))  
    { 
        // reciprocal is less than 0.5 
    }

This code tests to see if the reciprocal of x is less than 0.5 (or, conversely, that x is greater than 2). If the x variable has value 0 then the test 1/x is an error but, in this case, the expression will never be executed because the left operand to && is false.

Bitwise Shift Operators

Bitwise shift operators shift the bits in the left-hand operand integer the specified number of bits given in the right-hand operand, in the specified direction. A shift by one bit left multiplies the number by two, a shift one bit to the right divides by 2. In the following a 2-byte integer is bit-shifted:

    unsigned short s1 = 0x0010; 
    unsigned short s2 = s1 << 8; 
    std::cout << std::hex << std::showbase; 
    std::cout << s2 << std::endl; 
    // 0x1000  
    s2 = s2 << 3; 
    std::cout << s2 << std::endl; 
    // 0x8000

In this example, the s1 variable has the fifth bit set (0x0010 or 16). The s2 variable has this value, shifted left by 8 bits, so the single bit is shifted to the 13th bit, and the bottom 8 bits are all set to 0 (0x10000 or 4,096). This means that 0x0010 has been multiplied by 28, or 256, to give 0x1000. Next, the value is shifted left by another 3 bits, and the result is 0x8000; the top bit is set.

The operator discards any bits that overflow, so if you have the top bit set and shift the integer one bit left, that top bit will be discarded:

    s2 = s2 << 1; 
    std::cout << s2 << std::endl; 
    // 0

A final shift left by one bit results in a value 0.

It is important to remember that, when used with a stream, the operator << means insert into the stream, and when used with integers, it means bitwise shift.

Assignment Operators

The assignment operator = assigns an lvalue (a variable) on the left with the result of the rvalue (a variable or expression) on the right:

    int x = 10; 
    x = x + 10;

The first line declares an integer and initializes it to 10. The second line alters the variable by adding another 10 to it, so now the variable x has a value of 20. This is the assignment. C++ allows you to change the value of a variable based on the variable's value using an abbreviated syntax. The previous lines can be written as follows:

    int x = 10; 
    x += 10;

An increment operator such as this (and the decrement operator) can be applied to integers and floating-point types. If the operator is applied to a pointer, then the operand indicates how many whole items addresses the pointer is changed by. For example, if an int is 4 bytes and you add 10 to an int pointer, the actual pointer value is incremented by 40 (10 times 4 bytes).

In addition to the increment (+=) and decrement (-=) assignments, you can have assignments for multiply (*=), divide (/=), and remainder (%=). All of these except for the last one (%=) can be used for both floating-point types and integers. The remainder assignment can only be used on integers.

You can also perform bitwise assignment operations on integers: left shift (<<=), right shift (>>=), bitwise AND (&=), bitwise OR (|=), and bitwise exclusive OR (^=). It usually only makes sense to apply these to unsigned integers. So, multiplying by eight can be carried out by both of these two lines:

    i *= 8; 
    i <<= 3;