Skip to main content

Expressions

  • Classify operations into categories under rules of precedence
  • Categorize expressions as lvalues, xvalues and prvalues
  • Describe the features of expressions for each type-wise operator

"C [and hence C++] has an unusually rich set of operators that provide access to most of the operations provided by the underlying hardware." Harbison and Steele (2002)

Expressions specify computations. They consist of sequences of operators and operands and may produce a result, possibly with side effects. A C++ expression has a non-reference type and belongs to a value category. The expression may be primary or compound as illustrated below. A compound expression consists of an operator and one or more operands. A compound expression is ultimately decomposable into a set of primary expressions connected by operators.

Expressions

The order of evaluation of the operations in a compound expression determines the value of the expression. C++ defines this order through precedence rules for all of its operators.

This chapter describes the value categories of C++ expressions, the precedence rules that apply to operators in compound expressions and the specific features associated with each operator. Expressions based on bit-wise operators are described in a later chapter entitled Bit-Wise Expressions.

Value Categories

The value categories of C++ expressions are the basis for rules that compilers follow when creating, copying, and moving temporary objects during the evaluation of expressions. Awareness of these categories helps the programmer understand how the compiler evaluates expressions and interpret error messages that the compiler generates.

Any C++ expression is one of:

  • prvalue - a value that does not occupy a location in storage
  • xvalue - an expiring value that does occupy a location in storage (an object near the end of its lifetime)
  • lvalue - a locator value that occupies a location in storage

When the compiler creates a temporary object it converts a prvalue into an xvalue, which is temporarily located in memory - this is called a temporary materialization conversion.

Both lvalues and xvalues occupy a region of memory and together are called glvalues. A glvalue is a generalized lvalue. It evaluates to an object or a function. On the other hand, prvalues and xvalues are called rvalues. An rvalue is a temporary object or subobject, or a value not assocaited with an object.

In summary, C++17 categorizes expressions according to the taxonomy illustrated below. Any expression is either a glvalue or an rvalue.

Value Categories

Many expressions evaluate to prvalues, some to lvalues, some to xvalues and some to no value. The specific descriptions below identify the category to which each expression evaluates.

lvalue Operands

lvalue operands include:

  • a name that is not an array name
  • an array element - a[i]
  • (expression) - where expression is itself an lvalue
  • a direct member selection - expression.member - where expression is itself an lvalue
  • an indirect member selection - expression->member - where expression points to an lvalue
  • a dereferenced address - *expression
  • a string literal (actually an array of characters)

The operands associated with the following three operators must be lvalues:

  • &
  • ++
  • --

The left operands associated with the assignment operators must be lvalues:

  • =
  • +=
  • -=
  • *=
  • /=
  • %=

Operator Precedence

Expressions are divided into six operand-related classifications:

  • primary
  • postfix
  • prefix
  • unary
  • binary
  • ternary

Primary expressions are the fundamental expressions from which all other expressions are built. A primary expression consists of a single operand without any operator. The operand may be the name of an entity or a literal. Non-primary expressions consist of one operator and one or more operands. The operands may be primary or compound expressions.

Order of Evaluation

A compound expression evaluates according to rules defined through the precedence table below. These rules cannot be changed. The compiler evaluates the expression with the operator that has a higher precedence before evaluating any other expression. Some operators associate from left to right, while other operators associate from right to left.

PrecedenceClassOperatorDescriptionAssociates
18primaryname, literal
18binary::scope resolution
17postfix[]array subscriptingleft-to-right
17postfix( )function callleft-to-right
17postfix.direct selectionleft-to-right
17postfix->indirect selectionleft-to-right
17postfix++ --postfix increment, decrementleft-to-right
17postfixtypeid()type nameleft-to-right
17postfixconst_castconstant type conversionleft-to-right
17postfixdynamic_castdynamic type conversionleft-to-right
17postfixreinterpret_castreinterpreted type conversionleft-to-right
17postfixstatic_caststatic type conversionleft-to-right
16prefix++ --prefix increment, decrementright-to-left
16unarysizeofsizeright-to-left
16unary!logical negationright-to-left
16unary~bit-wise negationright-to-left
16unary- +arithmetic negation, plusright-to-left
16unary&address ofright-to-left
16unary*indirectionright-to-left
16unarynew new[]dynamic memory allocationright-to-left
16unarydelete delete[]dynamic memory deallocationright-to-left
16unaryalignof(type)alignment requirementright-to-left
16unarynoexcept()controls exception throwingright-to-left
16unary(type)type castingright-to-left
15binary.*object to member pointerleft-to-right
15binary->*pointer to member pointerleft-to-right
14binary* / %multiplicativeleft-to-right
13binary+ -additiveleft-to-right
12binary<< >>left and right bit shiftingleft-to-right
11binary< > <= >=relationalleft-to-right
10binary== !=equality, inequalityleft-to-right
9binary&bit-wise logical andleft-to-right
8binary^bit-wise logical exclusive orleft-to-right
7binaryǀbit-wise logical orleft-to-right
6binary&&logical andleft-to-right
5binaryǀǀlogical orleft-to-right
4ternary?:conditionalright-to-left
3binary= += -= *= /= %=compound type-wise assignmentright-to-left
3binary~= &= ^= ǀ= >>= <<=compound bit-wise assignmentright-to-left
2unarythrowtransfers control to exception handlerleft-to-right
1binary,sequencingleft-to-right

To override the order defined in this table, we enclose the expressions to be evaluated first within parentheses.

Postfix Expressions

Postfix expressions are expressions in which a postfix operator follows a single operand or the first operand of two operands. Postfix operators include the subscripting operator, the function call operator, the member selection operator, the postfix increment operator, and the postfix decrement operator.

Subscripting Operator

The subscripting operator takes as its left operand a pointer of type T and as its right operand a value of integral type. The expression identifies a modifiable lvalue at the offset from the address stored in the first operand specified by the second operand. The result of the operation is an lvalue of type T. For example,

double a[10];
int i = 2;

a[i] = 6; // lvalue

a holds the address of array a[10], i is an integral type, and a[i] refers to the i-th element beyond the start of a.

C++ accepts negative subscripts provided that the element is within the array's bounds. For example,

char  name[] = "Jane Doe";
char* surname = nullptr;

surname = &name[5];
surname[-1] = '.'; // changes "Jane Doe" to "Jane.Doe"

Member Selection

The member selection operators identify an object by their left operand and the name of a member by their right operand. A selection operation takes either of two forms: direct selection and indirect selection.

Direct Selection

The direct selection operator (.) takes an object of complete class type as its left operand and the member name as its right operand:

// Direct Selection
// direct.cpp

#include <iostream>

typedef struct
{
int x;
double y;
} S;

int main()
{
S s = {0, 0.0};

s.x = 1; // lvalue
std::cout << "x = " << s.x << "; y = " << s.y << std::endl;
}
x = 1; y = 0

Indirect Selection

The indirect selection operator (->) takes a pointer to an object of complete class type as its left operand and the member name as its right operand

// Indirect Selection
// indirect.cpp

#include <iostream>

typedef struct
{
int x;
double y;
} S;

S* add2(S *s)
{
s->x += 2;
s->y += 2;
return s;
}

int main()
{
S s = {0, 0};

add2(&s)->x++; // add2(&s) is modifiable
std::cout << "x = " << s.x << "; y = " << s.y << std::endl;
}
x = 3; y = 2

Postfix Increment and Decrement

The postfix increment and decrement operators change an lvalue by one unit after accessing the value. The operand is of integral, floating-point, or pointer type and followed by the increment (++) or decrement (--) operator. The operand must be a modifiable lvalue and the expression itself is a prvalue.

// Post-Fix Operators
// postfix.cpp

#include <iostream>

int main()
{
int i = 10;

std::cout << i++ << std::endl;
(i++)++; // ERROR (i++) is a prvalue, not an lvalue
std::cout << i << std::endl;
}

Results at Integer Limits

If the operand is of unsigned type and its value is 0u, the decrement operator changes the lvalue to the largest storable value. If the operand is of unsigned type and its value is the largest storable value, the increment operator changes the lvalue to 0u.

// Post-Fix Operators on Unsigned Values
// unsigned.cpp

#include <iostream>

int main()
{
unsigned i = 0u;

i--;
std::cout << i++ << std::endl;
std::cout << i << std::endl;
}
4294967295
0

If the operand is of signed integral type, floating-point type, or pointer type, and the increment/decrement operation produces an overflow/underflow, the result is undefined.

typeid() Operator (optional for OOP345)

The type identifier operator queries information about a type. The operator takes either a type or an expression and refers to a std::type_info that represents the dynamic type of the expression. The std::type_info object holds implementation-specific information about a type, which includes its name and the means to compare for equality. For an example of how this operator is used see the section entitled Dynamic Type Identification in the Chapter Inheritance and Inclusion Polymorphism.

Constrained Cast Operators

static_cast Operator

The static_cast<T>(v) operator converts expression v to type T. If T is an lvalue reference type or an rvalue reference to a function type, the result is an lvalue. If T is an rvalue reference to an object type, the result is an xvalue. Otherwise, the result is a prvalue.

The static_cast<T>(v) operator can perform the following conversions

  • Standard (Implicit) Conversions and their opposite
    • lvalue to rvalue
    • integral or floating-point promotions
    • conversions: integral, floating-point, floating-point to integral, integral to floating-point, pointer, pointer to member, boolean
    • void* to pointer type
  • between related classes
  • enumerator to integral or floating-point
  • scoped enumerator to integral or floating-point
  • rvalue references
  • any type to void discarding the value

The static_cast<T>(v) operator cannot cast away const-ness.

reinterpret_cast Operator

The reinterpret_cast<T>(v) operator converts expression v to type T. If T is an lvalue reference type or an rvalue reference to a function type, the result is an lvalue. If T is an rvalue reference to an object type, the result is an xvalue. Otherwise, the result is a prvalue.

The reinterpret_cast<T>(v) operator can perform the following conversions

  • pointer to member to pointer to member of different type
  • pointer to integral, integral or enumeration type to pointer
  • function pointer to function pointer of different type
  • object pointer to an object pointer of different type
  • function pointer to object pointer (conditionally supported)

The reinterpret_cast<T>(v) operator cannot cast away const-ness.

const_cast Operator

The const_cast<T>(v) operator converts expression v to type T. If T is an lvalue reference to an object type, the result is an lvalue. If T is an rvalue reference to an object type, the result is an xvalue. Otherwise, the result is a prvalue.

dynamic_cast Operator (optional for OOP345)

The dynamic_cast<T>(v) operator converts expression v to type T. T is a pointer or reference to a complete type. If T is a pointer type, v shall be an lvalue of a complete class and the result is an lvalue of the type referred to by T. If T is an rvalue reference to an object type, v shall be an glvalue of a complete class and the result is an xvalue of the type referred to by T.

The dynamic_cast<T>(v) operator cannot cast away const-ness.

For an example of how this operator is used see the section entitled Specializing an Operation for a Dynamic Type in the Chapter Inheritance and Inclusion Polymorphism.

Prefix Expressions

The prefix expressions are increment and decrement expressions in which the operator precedes the operand and associates from right to left.

Prefix Increment and Decrement

The prefix increment (++) and decrement (--) operators change an lvalue by one unit before accessing the value. The result of the operation is an lvalue, unlike their post-fix counterparts. The operand may be of any integral type, floating-point type, or pointer type, but must be a modifiable lvalue. For example,

// Pre-Fix Operators
// prefix.cpp

#include <iostream>

int main()
{
int i = 10;

std::cout << ++i << std::endl;
++(++i); // OK (++i) is an lvalue
std::cout << i << std::endl;
}
11
13

Results at the Limit

The results of a prefix increment and decrement operation on upper and lower limit values of unsigned types and signed types are similar to those for postfix increment and decrement operations.

If the operand is of unsigned type and its value is 0, the decrement operation evaluates to the largest storable value. If the operand in a prefix expression is of unsigned type and its value is the largest storable value, the increment operation evaluates to 0.

If the operand is of signed integral type, floating-point type, or pointer type, and the increment/decrement operation produces an overflow/underflow, the result is undefined.

Unary Expressions

The unary expressions are expressions in which the operator precedes a single operand. They include size evaluation, logical and bit-wise negation, arithmetic plus and negation, addressing, indirection, and type casting. Unary operators associate from right to left.

sizeof(), sizeof

The sizeof() operator evaluates the type of its operand and returns its size in bytes. The sizeof operator (without the parentheses) returns the number of bytes used by a variable, object, or expression. The result of either sizeof operation is a constant of type size_t (an unsigned integral type). For example,

// Size of a Type
// sizeof.cpp

#include <iostream>

int main()
{
double x;

std::cout << "On this machine, \n"
"the size of an int is " << sizeof(int)
<< "bytes,\nthe size of x is "
<< sizeof x << "bytes" << std::endl;
}
On this machine,
the size of an int is 4 bytes,
the size of x is 8 bytes.

sizeof() takes a type, while sizeof takes a variable, object, or expression. With some compilers, the syntax is interchangeable.

The compiler evaluates sizeof expressions at compile time. If its operand is an expression, the compiler does not evaluate that expression:

// sizeof Operator
// sizeof.cpp

#include <iostream>

int main()
{
int j = 1;

std::cout << sizeof j++ << std::endl;
std::cout << j << std::endl; // j is still 1
}
4
1

If the operand is an array name and the size of the array is within the scope of the expression, the sizeof operator returns the size of the array in bytes; that is, the number of elements in the array times the size of the type of one array element. However, if the operand is a function parameter that holds the address of an array, the operator returns the size of the pointer that holds the address, and not the size of the array:

// sizeof Operator
// sizeofArray.cpp

#include <iostream>

int foo(double a[])
{
a[0] = 1.0;
return sizeof a;
}

int main()
{
double a[] = {1.1, 2.2, 3.3, 4.4};

std::cout << sizeof a << std::endl;
std::cout << foo(a) << std::endl;
}
32
4

If the operand is an object of class type, the operation returns the sum of the sizes of the subobjects including any padding. The result is implementation dependent. For example,

// sizeof Operator
// sizeofStruct.cpp

#include <iostream>

typedef struct
{
char a;
int b;
} A;

int main()
{
A s;

std::cout << sizeof s << std::endl;
}
8

Logical Negation

The logical negation operator (!) converts its operand to a bool value and returns the converse of that value. The operand may be of integral type, floating-point type, or pointer type. The expression evaluates to a prvalue. If the operand is 0, 0.0 or nullptr, the expression evaluates to true. If the operand is any value other, the expression evaluates to false.

Bit-Wise Negation

See Bit-Wise Operators for the bit-wise operator (~).

Arithmetic Negation

The unary negation operator (-) evaluates to its operand with its sign reversed. The result is a prvalue of the same type as the operand after any promotions.

Results for Unsigned Types

The negation operator (-) applied to an operand of unsigned type deserves special attention. The result of the expression is of unsigned type and equal to 2n2^n-operand where nn is the number of bits used to store the operand:

// Arithmetic Negation
// minus.cpp

#include <iostream>
#include <iomanip>

int main()
{
unsigned a = 2u;
unsigned c = 1u;
unsigned e = 0u;

std::cout << a << std::setw(11) << -a << std::endl;
std::cout << c << std::setw(11) << -c << std::endl;
std::cout << e << std::setw(11) << -e << std::endl;
}
2 4294967294
1 4294967295
0 0

The arithmetic negation of an operand of signed integer type or floating-point type is undefined if overflow occurs.

Arithmetic Plus

The unary plus operator (+) evaluates to its operand. The result is a prvalue of the same type as the operand after any promotions. The operator is present in the language for symmetry.

Address of

The address-of operator (&) returns a pointer to its operand: the value is the address of the operand. The operand must be an lvalue that is not of storage class register. The expression is a prvalue.

We cannot take the address of a bit field and there are no pointers to bit fields of user-defined type.

Indirection

The indirection operator (*) returns the value stored in the address that the operand contains. The expression is itself an lvalue.

The indirection and address-of operators are inverses of one another. The result of *&x is x.

// Indirection
// indirection.cpp

#include <iostream>
#include <iomanip>

int main()
{
int x = 3;
int* p = &x;
std::cout << x << std::setw(11) << p << std::endl;

*&x = 4;
std::cout << x << std::setw(11) << p << std::endl;
}
3   002BF824
4 002BF824

Cast

The type-cast operator consists of a type name followed by an operand and converts the operand to the specified type. The operand may not be the name of an array. The expression is a prvalue.

There are two styles of casting:

  • C-style casts - enclose the target type within parentheses
  • C++-function-style casts - enclose the operand within parentheses

We may C-style cast an operand of integral or floating-point type to an integral or floating-point type; an operand of pointer type to any other pointer type or any integral type; and an operand of integral type, generic pointer type, or non-generic pointer type to a pointer type.

We may function-style cast an operand of integral or floating-point type to an integral or floating-point type; and an operand of pointer type to any integral type.

alignof() Operator

The alignof() operator returns the alignment requirement of its operand. The operand may be a type identifier representing the type of a complete object, or an array or a reference to one of those types. The operation returns an integral constant of type std::size_t.

For an example, see section entitled Type Alignment in the Chapter Fundamental Types.

decltype() Specifier

The decltype() specifier evaluates to the type and value category of its argument. The operand may be an entity or an expression. The value category of the expression affects the type:

  • if the value category of the expression is an xvalue, then the type is T&&
  • if the value category of the expression is an lvalue, then the type is T&
  • if the value category of the expression is a prvalue, then the type is T
// decltype with expressions
// decltype_expre.cpp

#include <iostream>

int main()
{
int i = 3, j = 6;
double x = 4.5;
double& r = x;

decltype(i + j) y = i + j; // int y
decltype(i + x) z = i + x; // double z
decltype(r) s = r; // double& s

std::cout << y << std::endl;
std::cout << z << std::endl;
std::cout << s << std::endl;
}
9
7.5
4.5

noexcept() Operator

The no exception operator (noexcept()) returns true if the argument does not throw an exception. This compile-time function evaluates to a prvalue of type bool. This operator does not evaluate the argument passed to it. This operator returns false if the expression contains:

  • a call to function that does not have a non-throwing exception specification (noexcept), unless it is a constant expression
  • a throw expression
  • a dynamic_cast expression when the target type is a reference type and conversion needs a run-time check
  • a typeid expression when the argument is a polymorphic type

A function with a noexcept specification in its declaration should be a non-throwing function. If a function marked noexcept allows an uncaught exception to escape at run-time, the program terminates immediately. For further details, see the section on No Exceptions in the Chapter entitled Error Handling.

throw Operator

The throw operator (throw) identifies an exception to normal execution. The operator creates a temporary object from its operand and transfers control to the exception handler. The exception object is an lvalue of the type of the operand of the throw expression. This operand is analogous to an argument passed to a function and received in its parameter. For example,

// Throw Operator
// throw.cpp

#include <iostream>

int divide(int x, int y)
{
if (y == 0)
throw "divide by zero inadmissible";
else
return x / y;
}

int main ()
{
int dividend, divisor, quotient = 0;

std::cout << "Enter dividend : ";
std::cin >> dividend;
std::cout << "Enter divisor : ";
std::cin >> divisor;

//... execute function successfully or throw exception

quotient = divide(dividend, divisor);

// ... handle exception

std::cout << "Quotient = " << quotient << std::endl;
}

For details on handling an exception that has been thrown see the section on Exceptions in the Chapter entitled Error Handling.

Binary Expressions

Binary expressions consist of two operands and one operator. They include arithmetic, relative, logical, and bit-wise expressions. They evaluate to prvalues.

Arithmetic

Multiplicative

A multiplicative expression evaluates the product, quotient, or remainder of its operands. The operator separates the operands. The operands are of integral or floating-point type for product and quotient operations (* /). The operands for the remainder operation (%) are of integral or unscoped enumeration type; the remainder operation (%) is undefined for operands of floating-point type.

Addition

The addition operator (+) adds the values of its operands. The operands may be of integral type, floating-point type, or non-generic pointer type. If one operand is of pointer type, the other must be of integral or unscoped enumeration type. For additions to an operand of pointer type see the section entitled Expressions in the chapter Raw Pointers.

Subtraction

The subtraction operator (-) subtracts the value of its second operand from the value of its first operand. The operands may be of integral type, floating-point type, or non-generic pointer type. If the left operand is of pointer type, the right operand must be of integral, unscoped enumeration or pointer type. For subtractions from an operand of pointer type see the section entitled Expressions in the chapter Raw Pointers.

Bit-Shifting

See the chapter entitled Bit-Wise Expressions for the bit-shifting operators (<< >>).

Relational

A relational expression compares the values of two operands for relative magnitude, equality, or inequality. The operands are of arithmetic, enumeration, or pointer type. If one of the operands is of pointer type, then the other operand must be of compatible pointer type or the nullptr constant. A relational expression is of type bool and evaluates to either true or false.

Less-Than

The less-than operators (<, <=) compare the relative magnitude of their operands' values.

Greater-Than

The greater-than operators (>, >=) compare the relative magnitude of their operands' values.

Equality

The equality operator (==) compares the values of its operands for identity.

Inequality

The inequality operator (!=) compares the values of its operands for lack of identity.

If the right operand is zero-valued, we may omit it and the operator and thereby convert the left operand into a truth evaluation. That is, the expression in the first sample below evaluates to the same result as the expression on the second one:

if (x != 0)
{
// ...
}
if (x)
{
// ...
}

Bit-Wise and

See the chapter entitled Bit-Wise Expressions for the bit-wise and operator (&).

Bit-Wise or

See the chapter entitled Bit-Wise Expressions for the bit-wise or operator (|).

Bit-Wise Exclusive or

See the chapter entitled Bit-Wise Expressions for the bit-wise exclusive or operator (^).

Logical

Binary logical expressions evaluate the combined truthfulness of their operands. The operands may be of integral type, floating-point type, or pointer type. The type of one operand may differ from the type of the other. Logical expressions evaluate to values of type bool: false or true.

Intersection and Union

The logical 'and' operator (&&) compares its operands and returns their intersection. The following rules apply:

  • true, if both operands are true
  • false, if either of the operands is false

The logical 'or' operator (||) compares its operands and returns their union. The following rules apply:

  • true, if either operand is true
  • false, if both of the operands are false

Left to Right

Logical expressions evaluate their operands left-to-right as necessary. In this respect, they differ from other binary expressions. If the left operand determines the result, the right operand is not evaluated. For instance, if the left operand is false and the operator is &&, the right operand is not evaluated. Similarly, if the left operand is true and operator is ||, the right operand is not evaluated.

The following program does not increment either j or i:

// Logical Expressions
// logical.cpp

#include <iostream>

int main()
{
int i = 1, j = 1;

std::cout << (i < 2 || j++ == 1) << ' ' << j << std::endl;
std::cout << (j == 2 && i++ == 1) << ' ' << i << std::endl;
}
1 1
0 1

Order Matters

Because logical expressions guarantee left to right evaluation, the order of their operands matters. Consider the expression

int* a = nullptr;
if ( nullptr != a && 6 == a[i] ) // OK - Correct Order
std::cout << "a[" << i << "] is " << a[i] << std::endl;
else
std::cout << "Memory not allocated!" << std::endl;

This code will not crash since a != nullptr. However, if we switched the order of the two conditions, we would expose the possibility of a crash at the evaluation of a[i]:

int* a = nullptr;
if ( 6 == a[i] && nullptr != a ) // ERROR - Wrong Order
std::cout << "a[" << i << "] is " << a[i] << std::endl;
else
std::cout << "Memory not allocated!" << std::endl;

Assignment Expressions

An assignment expression copies from the right operand to the left operand. The left operand must be a modifiable lvalue. Because assignment expressions associate from right to left, cascading is possible. The expressions may be simple or compound.

Simple Assignment

The simple assignment operator (=) copies the value of the right operand without modification into the left operand. We can cascade the assignment to several variables of a single value:

// Assignment Expressions
// assignment.cpp

#include <iostream>

int main()
{
int i, j, k;

i = j = k = 3;
std::cout << i << ' ' << j << ' ' << k << std::endl;
}
3 3 3

If we encapsulate an array in a class type, we can copy all of its elements in a simple assignment. For example,

// Assignment Expressions
// copyArray.cpp

#include <iostream>

typedef struct
{
int a[20];
} Array;

int main()
{
Array original = {1, 2, 3, 4, 5}, copy;

copy = original;

for (int i = 0; i < 5; i++)
std::cout << copy.a[i] << ' ';
std::cout << std::endl;
}
1 2 3 4 5

Compound Assignment

The compound operators (+= -= *= /= %= >>= <<= ~= &= |= ^=) perform the operation specified by the first character in the operator on the two operands before copying the result into the left operand.

Sequential Expressions

The comma operator (,) divides expressions for separate evaluation in sequence from the left. The operands may be expressions themselves and may be of any type. The type and value of the result of a sequential expression is that of its rightmost operand.

A common use of the comma operator is in definitions and iteration constructs

void reverse(char *s)
{
char t;

for (int i = 0, j = strlen(s) - 1; i < j; i++, j--)
{
t = s[i];
s[i] = s[j];
s[j] = t;
}
}

Mixed-Type Binary Expressions

The above descriptions apply to binary expressions on operands of compatible type. If the operands in the expression are not of the type defined for the operator, one of the operands must be converted to a compatible type. The compiler promotes operands types if necessary to evaluate a binary expression. It converts the operand of lower type to a higher type using the following hierarchy:

  
long doublehighest
double...
float...
unsigned long long...
long long...
unsigned long...
long...
unsigned int...
int...
unsigned char...
char...
boollowest

If one pointer is of generic (void*) type and the operation requires pointers of compatible type, the compiler converts the non-generic pointer to generic (void*) type.

The Unsigned Trap

If one of the operands is of unsigned integral type and the other is of a signed integral type, the compiler promotes the value of the signed type to a value of unsigned type. This particular promotion may produce a counter-intuitive result. For example,

// Relational Expressions
// relational.cpp

#include <iostream>

int main()
{
unsigned len = 0u;

std::cout << (len > -1) << std::endl; // might expect true
// evaluates to false
}
0

The expression in parentheses has a value of false (0) because the compiler promoted -1 to the largest unsigned integer 4294967295u before evaluating the expression.

Good Design Practice

Avoid Implicit Promotions

Implicit promotions are not necessarily portable. Type promotions may vary across platforms. Hence, it is preferable to cast the operand of lower type explicitly.

Sub-Divide Complex Binary Expressions

Complex binary expressions are not always portable. Consider

int x = 3;
x = x + ++x; // the result may be either 7 or 8

The order of evaluation of the operands in a binary expression is implementation dependent. To ensure identical results on all platforms, we subdivide complex binary expressions into multiple statements that clarify our intent:

int x = 3;
++x;
x = x + x; // increment first = 8
int x = 3, y;
y = x;
++x;
x = y + x; // add using the old value of x = 7

Ternary Expression

The ternary expression is a conditional expression. The first operator (?) identifies the expression as a selection construct; the second operator (:) separates the choices.

The leftmost operand may be of integral type, floating-point or pointer type. If it evaluates to true, the expression evaluates the second operand, but not the third operand. If the leftmost operand evaluates to false, the expression evaluates the third operand, but not the second operand. The result is a value of the type of the evaluated operand.

The conditional expression provides a simple method of avoiding multiple returns:

int sign(int x)
{
return x < 0 ? -1 : 1;
}

If the second or third operands are not primary expressions, the compiler introduces run-time code to evaluate them based upon the truth value of the leftmost operand. Some compilers require that the second and third operands be of compatible type.

Since the conditional operator has low precedence, using it within another expression usually requires enclosure within parentheses.