Skip to content
VLSI Mentor

C++ Part I · Module 4 · Expressions and Operators

Arithmetic Operators and the Type of a Result

The arithmetic operators are the part of C++ that looks like it needs no explanation. You already know what plus does. The interesting question is not what value an arithmetic expression produces but what type it produces, and the answer is frequently not the type of either operand. Add two 8-bit values and you get a 32-bit int. Add a signed value to an unsigned one and the result is unsigned, with everything that implies from Chapter 3.3. Add an int to a double and the int is converted first. These are not special cases to memorise individually — they are two rules that C++ applies to every arithmetic expression before the operator does any work at all. Chapter 3.3 needed a cast and did not say why; Chapter 3.4 called it arithmetic width and handed it here. This is where both debts are paid.

Foundation15 min readC++Part IFundamentalsOperators

Part I · Module 4 · Chapter 4.2 · Arithmetic Operators and the Type of a Result

1. The Operators

Five binary arithmetic operators and two unary ones:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    a + b     // addition
    a - b     // subtraction
    a * b     // multiplication
    a / b     // division
    a % b     // remainder

    -a        // negation
    +a        // unary plus — rarely useful

For the familiar cases these do what you expect. Two of them do not, and they have their own chapter:

  • / on integers truncates rather than producing a fraction.
  • % gives a remainder and its sign with negative operands surprises people.

Chapter 4.3 covers both, along with what division by zero does. This chapter is about something that applies to all seven: the type of the result.

2. The Question Worth Asking

Chapter 4.1 §2 established that an expression produces a value of a type. For arithmetic, people reason about the value and assume the type follows the operands.

It frequently does not. Here is the measurement:

Azvya Education Pvt. Ltd.VLSI Mentor
promo.cpp — complete program
#include <cstdint>
#include <iostream>
#include <type_traits>

int main()
{
    char         a = 10, b = 20;
    std::uint8_t x = 10, y = 20;
    short        s = 10, t = 20;
    unsigned int u = 10;
    int          i = 20;

    std::cout << "char  + char  is int?       " << std::is_same<decltype(a + b), int>::value << '\n';
    std::cout << "uint8 + uint8 is int?       " << std::is_same<decltype(x + y), int>::value << '\n';
    std::cout << "short + short is int?       " << std::is_same<decltype(s + t), int>::value << '\n';
    std::cout << "int + unsigned is unsigned? " << std::is_same<decltype(i + u), unsigned int>::value << '\n';
    std::cout << "int + double is double?     " << std::is_same<decltype(i + 1.0), double>::value << '\n';
    std::cout << "sizeof(a+b) = " << sizeof(a + b) << "  (sizeof(char) = " << sizeof(char) << ")\n";
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
char  + char  is int?       1
uint8 + uint8 is int?       1
short + short is int?       1
int + unsigned is unsigned? 1
int + double is double?     1
sizeof(a+b) = 4  (sizeof(char) = 1)

All ones. Read the last line twice: both operands occupy one byte and the result occupies four.

Two rules produce all of that, and they run in order.

3. Rule One: Integer Promotion

Before arithmetic, any integer type narrower than int is converted to int.

That covers char, signed char, unsigned char, short, unsigned short, bool, and therefore std::uint8_t and std::uint16_t — which Chapter 3.6 §7 established are character and short types underneath.

This is called integer promotion, and it happens whether you want it or not. There is no arithmetic on 8-bit values in C++; there is arithmetic on int values that came from 8-bit objects.

The reason is historical and practical: int is meant to be the size the machine computes with most naturally, so promoting small types to it costs nothing and avoids defining arithmetic separately for every narrow type.

What it explains

Chapter 3.3 §4 needed a cast, and now you can see why:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::uint8_t packet_count = 255;
    packet_count = static_cast<std::uint8_t>(packet_count + 1);

packet_count + 1 promotes packet_count to int, adds 1, and produces the int value 256. There is no wraparound at this point — an int holds 256 comfortably. The wraparound happens when that 256 is converted back to std::uint8_t on assignment.

So the modulo-2⁸ behaviour Chapter 3.3 described is real, and it happens at the assignment rather than in the addition. Without the cast you are storing an int into a std::uint8_t, which Chapter 3.7 §4 identified as a narrowing conversion.

Chapter 3.4 §5's "arithmetic width" is this rule. A 5-bit field stored in a std::uint8_t is computed on as an int, and nothing about the storage type constrains the arithmetic.

4. Rule Two: The Usual Arithmetic Conversions

Promotion handles types narrower than int. The second rule handles everything else:

If the two operands still have different types after promotion, both are converted to a common type before the operator runs.

This is called the usual arithmetic conversions. The full ranking has more steps than you need now; three consequences cover almost everything:

OperandsResult typeWhy
int and doubledoubleFloating point wins over integer
int and unsigned intunsigned intAt equal rank, unsigned wins
int and longlongThe wider type wins

The second row is the one that causes bugs, and it is Chapter 3.3 §7's trap restated as a rule about types:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int          offset = -1;
    unsigned int length = 4;
    offset < length           // offset is converted to unsigned FIRST

Chapter 3.3 showed the result and deferred the mechanism. The mechanism is this rule: at equal rank the signed operand is converted to unsigned, and −1 becomes a very large number by the modulo-2ᴺ rule. Chapter 4.4 works the comparison case through fully.

The first row is the useful one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    double average = total_cycles / 1.0;     // int / double → double

5. A Worked Example

Azvya Education Pvt. Ltd.VLSI Mentor
mix.cpp — complete program
#include <cstdint>
#include <iostream>

int main()
{
    std::uint8_t  low_byte   = 200;
    std::uint8_t  high_byte  = 100;
    int           transfers  = 7;
    double        clock_mhz  = 133.5;

    std::cout << "u8 + u8          = " << (low_byte + high_byte) << '\n';
    std::cout << "stored back in u8= "
              << static_cast<unsigned>(static_cast<std::uint8_t>(low_byte + high_byte)) << '\n';
    std::cout << "int * double     = " << (transfers * clock_mhz) << '\n';
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u8 + u8          = 300
stored back in u8= 44
int * double     = 934.5

Three lines, three lessons.

200 + 100 gave 300, not 44. Both operands were promoted to int, so the addition had room. The result type is int.

Storing it back gave 44. 300 modulo 256 is 44. The loss happened at the conversion into std::uint8_t, exactly where Chapter 3.3 §4 said unsigned wraparound occurs — and nowhere else.

7 * 133.5 gave 934.5. The int was converted to double and the multiplication happened in floating point.

Notice what you can now predict without running anything: the type of each result, and therefore where a value can and cannot be lost.

6. Where This Chapter Stops

The conversions above happen before an arithmetic operator runs, and this chapter covers them because the result type cannot be explained without them.

Conversions as a subject are larger than that. They also happen when you assign, when you initialise, when you pass an argument, and when you ask for one explicitly with a cast. Chapter 4.10 owns all of it, including:

  • making a conversion visible with static_cast rather than letting it happen silently,
  • why a C-style cast is a poor way to say the same thing,
  • and how narrowing (Chapter 3.7) fits into the wider picture.

For now you have the two rules that govern arithmetic, and that is enough to reason about any expression in this module.

7. One Thing Arithmetic Does Not Save You From

A reminder rather than a new rule, because it combines with everything above.

Chapter 3.3 §5 established that signed integer overflow is undefined behaviour. Promotion does not change that — it moves the arithmetic to int, and int can still overflow:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int cycle_count = 2'000'000'000;
    int doubled = cycle_count * 2;      // overflows int: undefined behaviour

Both operands are already int, so no promotion occurs and no conversion saves you. The product does not fit and the program has no defined meaning.

The fix is to compute in a type that is wide enough, which you choose deliberately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::int64_t doubled = static_cast<std::int64_t>(cycle_count) * 2;

Note where the cast goes: on an operand, not on the result. Casting the result would be too late — the overflow would already have happened in int arithmetic before anything was converted. This is a direct consequence of §2's point that the operator works on the converted operand types.

8. Mistakes Worth Making Once

Assuming the result has the operands' type. Two std::uint8_t values added produce an int. Measured: sizeof(a+b) is 4 while sizeof(char) is 1.

Expecting small-type arithmetic to wrap during the operation. It does not — promotion gives it room. The wrap happens when you store the result back into the narrow type.

Forgetting that int meets unsigned as unsigned. This is Chapter 3.3's trap as a type rule, and Chapter 4.4 shows it biting.

Casting the result instead of an operand. static_cast<std::int64_t>(a * b) computes a * b in the original type first. If that overflows, the cast arrives too late.

Thinking promotion protects against overflow generally. It only helps types narrower than int. int * int has no wider type to be promoted to.

Mixing integer and floating-point operands without noticing. It works and the result is floating point — which is usually what you wanted, and occasionally is how a value you expected to be exact becomes an approximation (Chapter 3.5).

9. What You Should Be Able to Say Now

  1. The arithmetic operators are + - * / % and unary - and +. Division and remainder have their own chapter.
  2. The interesting question is the type of the result, not only its value.
  3. Integer promotion: any integer type narrower than int is converted to int before arithmetic. That includes char, short, bool, std::uint8_t and std::uint16_t.
  4. Measured: char + char, uint8 + uint8 and short + short all produce int, and sizeof(a+b) is 4 while sizeof(char) is 1.
  5. There is no 8-bit arithmetic in C++ — only arithmetic on int values that came from 8-bit objects.
  6. Usual arithmetic conversions: operands that still differ after promotion are brought to a common type. int with double gives double; int with unsigned int gives unsigned int; int with long gives long.
  7. That unsigned rule is the mechanism behind Chapter 3.3 §7's surprising comparison. Chapter 4.4 completes it.
  8. Promotion is protective: 200 + 100 in uint8_t objects gives 300 rather than overflowing, and the loss happens visibly at the store-back (44).
  9. These conversions happen before the operator runs. Conversions as a subject — casts, assignment, narrowing — are Chapter 4.10.
  10. Promotion does not prevent int overflow, which remains undefined behaviour.
  11. To compute in a wider type, cast an operand, not the result.

10. Exercises

Exercise 1 — name the result type. Given std::uint8_t b, short s, int i, unsigned u, long l, double d: (a) b + b (b) s * 2 (c) i + u (d) i + l (e) i / d (f) b + d.

Exercise 2 — predict two values. With std::uint8_t x = 200, y = 100; say what x + y produces and what static_cast<std::uint8_t>(x + y) produces, and name where the loss occurs.

Exercise 3 — explain the old cast. Chapter 3.3 §4 wrote static_cast<std::uint8_t>(packet_count + 1). Explain what the addition produced, why no wraparound happened there, and what the cast is actually doing.

Exercise 4 — fix the overflow. int cycles = 2'000'000'000; int doubled = cycles * 2; is undefined behaviour. Give a correct version and explain why putting the cast around cycles * 2 would not work.

Exercise 5 — the unsigned surprise, early. Without reading Chapter 4.4, predict the type of i + u where i is int and u is unsigned int, and say what that implies if i is negative.

Exercise 6 — why promotion helps. Argue that integer promotion makes narrow-type arithmetic safer rather than more confusing, using the 200 + 100 case. Then state the cost it imposes on the programmer.

Exercise 7 — the accidental double. int total = 7; int count = 2; auto average = total / count; gives 3. A colleague changes count to 2.0 and the result becomes 3.5. Explain both results in terms of the rules in this chapter, and say what the type of average is in each case.

11. Summary

  • The operators are the easy half. The result's type is where the engineering is.
  • Integer promotion: types narrower than int become int before arithmetic — char, short, bool, std::uint8_t, std::uint16_t.
  • Measured: two one-byte operands produce a four-byte result.
  • There is no narrow-type arithmetic in C++, only arithmetic on promoted values.
  • Usual arithmetic conversions then bring differing operands to one type: floating point beats integer, unsigned beats signed at equal rank, wider beats narrower.
  • int meets unsigned as unsigned — the mechanism behind Chapter 3.3's trap, completed in Chapter 4.4.
  • Promotion is protective: 200 + 100 gave 300, and the wrap to 44 happened visibly at the store-back.
  • These conversions precede the operator. Conversions as a subject are Chapter 4.10.
  • Promotion does not stop int overflow, which is still undefined behaviour.
  • Cast an operand, not the result — by the time you cast a result the arithmetic has already happened.

Two of the operators listed in §1 were set aside, and they are the two that behave least like the arithmetic you learned at school. Integer division throws away the fractional part rather than rounding it, which means the answer to 7 / 2 is not 3.5 and is also not 4. The remainder operator interacts with that truncation in a way that decides its sign, and the rule is not the one most people guess when the operands are negative. Chapter 4.3 takes both, along with the one arithmetic operation that has no defined result at all.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.