Embercode
✕
Character Arrays and StringsLesson 14 of 16

45 min read

Formatting output

Why this matters

A program often prints results for a person to read. A price written as 13.5 looks wrong next to 4.50. Names of different lengths push the next column out of line, so a list of items looks like a jumble. Formatting means choosing how each value looks on screen: how many characters it takes, how many digits follow the decimal point, and which side of its space it sits on.

Two words come up again and again in this lesson. The width of a value is the number of characters it takes up. If the value is shorter than the width, spaces fill the gap. The precision of a decimal number is how many digits come after the decimal point.

Earlier lessons covered how std::string stores text. This lesson covers how a program turns numbers and text into printed characters that line up. It builds on std::string, on std::vector for lists, and on for loops.

The idea: a template with holes

A format string is a piece of text with holes in it. Each hole is written as a pair of braces, {}. You give the values in order, and the library puts each value into the next hole. The result is a new string.

Here is the smallest example, using std::format from the <format> header. It is part of the C++20 standard.

#include <format>
#include <iostream>
#include <string>
 
int main() {
    std::string name = "Ada";
    std::string line = std::format("Name: {}", name);
    std::cout << line << "\n";
}

The call std::format("Name: {}", name) returns the string Name: Ada. The hole {} takes the value name. The line std::cout << line prints the result. Nothing is printed by std::format itself. It only builds the string, and you decide what to do with it.

Inside a hole you can add a rule after a colon. The rule says how the value should look. In {:>8} the colon starts the rule, > means right-align, and 8 is the width. The next example uses several rules.

Example 1: basic rules for text and numbers

This program shows the most common rules. Each std::format call builds one line, and the program prints it.

#include <format>
#include <iostream>
#include <string>
 
int main() {
    std::string name = "Ada";
    int score = 42;
    double price = 3.14159;
 
    std::cout << std::format("Name: {}\n", name);
    std::cout << std::format("[{:>8}]\n", name);
    std::cout << std::format("[{:<8}]\n", name);
    std::cout << std::format("[{:^8}]\n", name);
    std::cout << std::format("[{:05}]\n", score);
    std::cout << std::format("[{:.2f}]\n", price);
    std::cout << std::format("[{:8.2f}]\n", price);
    std::cout << std::format("[{:<8.2f}]\n", price);
}

Compile and run it with the C++20 standard and warnings turned on:

$ g++ -std=c++20 -Wall example1.cpp -o example1 && ./example1
Name: Ada
[     Ada]
[Ada     ]
[  Ada   ]
[00042]
[3.14]
[    3.14]
[3.14    ]

Each rule works like this:

  • {} with no rule prints the value the normal way.
  • {:>8} right-aligns the text in 8 characters, so the spaces go on the left.
  • {:<8} left-aligns it, so the spaces go on the right.
  • {:^8} centres it. When the gap has an odd number of spaces, the extra space goes on the right.
  • {:05} pads a whole number with zeros on the left to make 5 characters.
  • {:.2f} prints a decimal number with exactly 2 digits after the point. The letter f means fixed-point, which is the usual decimal form.
  • {:8.2f} does both: 2 digits after the point and a width of 8. The number is right-aligned by default.
  • {:<8.2f} is the same, but left-aligned.

Numbers are right-aligned by default and text is left-aligned by default.

Hex and binary

The same rules print a number in other bases. A base is the set of digits a number system uses. Decimal is base 10. Hexadecimal, written hex, is base 16 and uses the digits 0 to 9 and the letters a to f. Binary is base 2.

#include <format>
#include <iostream>
 
int main() {
    std::cout << std::format("[{:x}] [{:X}] [{:#x}] [{:b}]\n", 255, 255, 255, 5);
    std::cout << std::format("[{:+d}] [{:+d}]\n", 42, -42);
}
[ff] [FF] [0xff] [101]
[+42] [-42]

The letter x prints lower-case hex and X prints upper-case hex. The # flag adds the prefix 0x. The letter b prints binary. The + flag always shows a sign, even on a positive number.

Example 2: a receipt with dynamic width

A rule can take its width from a value instead of a fixed number. Put another pair of braces inside the rule. The inner braces are filled from the argument list, so they use up one value each.

The next program prints a small receipt. Each item name is left-aligned in a column whose width is stored in a variable. The quantity and the price are right-aligned.

#include <format>
#include <iostream>
#include <string>
#include <vector>
 
struct Item {
    std::string name;
    int quantity;
    double unit_price;
};
 
int main() {
    std::vector<Item> items = {
        {"Notebook", 3, 4.5},
        {"Pen", 12, 0.75},
        {"Stapler with a long name", 1, 12.0},
    };
 
    int name_width = 12;
    std::cout << std::format("{:<{}} {:>4} {:>8}\n", "Item", name_width, "Qty", "Price");
    std::cout << std::string(name_width + 14, '-') << "\n";
 
    double total = 0.0;
    for (const Item& item : items) {
        double line = item.quantity * item.unit_price;
        total += line;
        std::cout << std::format("{:<{}.{}} {:>4} {:>8.2f}\n",
                                 item.name, name_width, name_width,
                                 item.quantity, line);
    }
    std::cout << std::format("{:>{}} {:>8.2f}\n", "Total", name_width + 5, total);
}
Item          Qty    Price
--------------------------
Notebook        3    13.50
Pen            12     9.00
Stapler with    1    12.00
            Total    34.50

Look at the first line of the header. The rule {:<{}} uses two values. The first value, "Item", is printed. The second value, name_width, becomes the width. Then {:>4} takes the next value, "Qty", and {:>8} takes "Price". So the values are used in the order the holes appear, and a nested hole uses up a value like any other hole.

The rule in the item line is {:<{}.{}}. The first {} is the width and the second is the precision. For text, precision sets the most characters that can be printed. Here it cuts Stapler with a long name down to Stapler with, so the long name cannot push the columns out of line.

The total is right-aligned under the price column. Its width is name_width + 5, which covers the name column plus the quantity column, so the number ends in the price column.

Decimal numbers and rounding

A decimal value such as 2.675 is stored inside the computer in binary, which is base 2. Some decimal values, including 2.675, cannot be stored exactly in binary. The stored value is a little below or above the decimal value you wrote. Rounding then uses the stored value, so the output can surprise you.

#include <format>
#include <iostream>
 
int main() {
    std::cout << std::format("[{:.2f}] [{:.2f}] [{:.1f}]\n", 2.675, 1.005, 0.25);
    std::cout << std::format("[{:.2f}]\n", 2.5);
}
[2.67] [1.00] [0.2]
[2.50]

The number 2.675 prints as 2.67, not 2.68. The number 0.25 with one digit prints as 0.2, because the library rounds an exact half to the even digit. The number 2.5 with two digits is simply 2.50.

Keep this in mind when you print money. Many real programs store amounts as whole cents in an integer, so no rounding happens while the program adds up the total. That is a topic of its own. This lesson only shows the output rules.

iostream manipulators

Before std::format existed, C++ programs printed with std::cout. cout is the stream that sends text to the screen. A stream is a channel that text flows through, one << at a time. A manipulator is a small object you place in the stream to change how the next value prints. The manipulators live in the <iomanip> header.

ManipulatorWhat it does
std::setw(n)Sets the width of the next value only
std::setprecision(n)Sets the digits after the point for decimal numbers
std::fixedPrints decimals with a fixed number of digits after the point
std::left and std::rightChoose the side for alignment
std::setfill(c)Chooses the padding character

The next program prints the same kind of values as Example 1, using manipulators.

#include <iomanip>
#include <iostream>
#include <string>
 
int main() {
    std::string name = "Ada";
    double price = 3.14159;
 
    std::cout << std::setw(8) << std::left << name << "|\n";
    std::cout << std::setw(8) << std::right << name << "|\n";
    std::cout << std::fixed << std::setprecision(2) << price << "\n";
    std::cout << std::setw(8) << price << "|\n";
    std::cout << std::setfill('0') << std::setw(5) << 42 << "\n";
    std::cout << std::setfill(' ');
    std::cout << std::setw(5) << 42 << "|\n";
}
Ada     |
     Ada|
3.14
    3.14|
00042
   42|

Some manipulators stick and some do not. std::left, std::right, std::fixed, std::setprecision and std::setfill stay in effect until you change them. In the program above, std::setfill('0') changes the padding for the number 42 and stays on for the next line, so the program sets it back to a space before the last line.

std::setw is different. It applies to the next value only, and then it resets. The next program shows this.

#include <iomanip>
#include <iostream>
 
int main() {
    std::cout << std::setw(5) << 1 << 2 << "|\n";
}
    12|

The width of 5 is used for the 1. The 2 is printed with no width at all. So setw must go in front of every value you want padded.

printf

printf is the older way to format output in C. It is still common, and you will meet it in C++ code. It is declared in the <cstdio> header. Instead of {}, it uses a percent code for each hole. A percent code is a % followed by letters that say what kind of value goes there.

#include <cstdio>
 
int main() {
    std::printf("[%-8s]\n", "Ada");
    std::printf("[%8s]\n", "Ada");
    std::printf("[%08d]\n", 42);
    std::printf("[%.2f]\n", 3.14159);
    std::printf("[%8.2f]\n", 3.14159);
    std::printf("[%x] [%X] [%#x]\n", 255, 255, 255);
    std::printf("[%+d]\n", 42);
    std::printf("[%.3s]\n", "abcdef");
    std::printf("[%*d]\n", 6, 42);
}
[Ada     ]
[     Ada]
[00000042]
[3.14]
[    3.14]
[ff] [FF] [0xff]
[+42]
[abc]
[    42]

The codes map onto the same ideas as before. %-8s is a string with a width of 8, left-aligned. %8s is right-aligned. %08d is a whole number padded with zeros to 8 characters. %.2f is a decimal with 2 digits after the point. %.3s cuts a string to 3 characters. %*d takes the width from the argument list, the same way the nested braces did in std::format. The * is replaced by the number 6 that comes before the value.

printf does not check that a value matches its code. The compiler can check some cases when you turn on warnings. The next example gives %d, which means an integer, a decimal number instead.

#include <cstdio>
 
int main() {
    std::printf("[%d]\n", 3.5);
}

With -Wall, g++ reports a warning:

warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘double’ [-Wformat=]

Warnings do not stop the build, so the program still runs. In one run it printed [-280238511], which is not 3.5 at all. Your run may print a different number. The C++ standard calls this undefined behaviour. It means the language makes no promise about what happens, so the output can change from run to run or from one compiler to another. std::format avoids this problem because it checks the types when you compile. That is one reason to prefer it in new code.

Java's String.format and printf

Java has the same two ideas. String.format builds a string from a format string and values, and printf prints the result. Java uses percent codes, like C's printf. The course's Java track covers them in detail. Two results are useful to know when you read C++ code next to Java code. String.format("[%-8s]", "Ada") gives [Ada ], the same padding as the C version. String.format("%d", 3.5) throws an exception named IllegalFormatConversionException with the message d != java.lang.Double. Java checks the type when it runs, so the mistake shows up as an error, not as a wrong number.

Common mistakes

Using setw for more than one value

std::setw resets after each value, as the example above showed. A line such as std::cout << std::setw(5) << name << price; pads the name and not the price. Put std::setw before each value you want padded.

Forgetting the include

Using std::setw without <iomanip> gives this error from g++:

error: ‘setw’ is not a member of ‘std’
    3 |     std::cout << std::setw(4) << 1 << "|\n";
note: ‘std::setw’ is defined in header ‘<iomanip>’; this is probably fixable by adding ‘#include <iomanip>’

The compiler names the header you need. Add #include <iomanip> at the top of the file.

Too few values

Each {} needs one value. If there are fewer values than holes, std::format will not compile. The string is checked at compile time, so the error appears before the program runs:

error: call to consteval function ‘std::basic_format_string<char, int>("{} {}\012")’ is not a constant expression

The \012 in the message is the newline at the end of the format string, shown as an octal code. Read the first error line. It names the format string that failed.

Too many values

Extra values are not an error. This program compiles with -Wall and runs without a warning:

#include <format>
#include <iostream>
 
int main() {
    std::cout << std::format("{}\n", 1, 2);
}
1

The 2 is ignored. If you add a value and the output does not change, check that a hole is waiting for it.

A rule that does not fit the type

Precision means digits after the point, so it does not apply to a whole number. This is an error:

error: call to consteval function ‘std::basic_format_string<char, int>("{:.2}\012")’ is not a constant expression

The format string {:.2} is written for a decimal or text value. Use {:.2f} for a decimal number, or leave the precision out for an integer.

Mixing up argument types with printf

The printf example above gives a warning, and the output is a wrong number. Check that each percent code matches its value. For a string use %s, for an integer %d, and for a decimal %f. The %s code expects a C string, a plain character pointer, so pass name.c_str() when the value is a std::string.

Expecting exact decimal results

The example with 2.675 shows that a rounded value can differ from the number you expected. Do not write tests that compare a printed decimal with an exact text unless you know how the value was rounded.

Practice

Exercise 1. Print Total: 7.5 so it shows exactly two digits after the point, using std::format.

Show the answer

Use std::format("Total: {:.2f}\n", 7.5). The output is Total: 7.50.

Exercise 2. Print the number 255 in hexadecimal with the prefix 0x, padded with zeros to a total width of 6.

Show the answer

Use std::format("{:#06x}\n", 255). The output is 0x00ff. The width of 6 includes the 0x prefix.

Exercise 3. Print the names Ada, Grace and Linus one per line, each right-aligned in a width of 7 and wrapped in square brackets.

Show the answer

#include <format>
#include <iostream>
#include <string>
#include <vector>
 
int main() {
    for (std::string name : std::vector<std::string>{"Ada", "Grace", "Linus"}) {
        std::cout << std::format("[{:>7}]\n", name);
    }
}

The output is:

[    Ada]
[  Grace]
[  Linus]

Exercise 4. A program prints an age and a name with printf, but the values are in the wrong order. Fix the call so the output reads Ada is 36 years old. The variables are std::string name = "Ada"; and int age = 36;.

std::printf("%s is %d years old\n", age, name);

Show the answer

Swap the arguments and pass the C string from the std::string:

std::printf("%s is %d years old\n", name.c_str(), age);

The output is Ada is 36 years old. The %s code needs a C string, which name.c_str() returns. Passing age where %s expects text is undefined behaviour, so the output cannot be trusted.

Exercise 5. Using std::setw, print the numbers 1 and 2 each in a width of 4 on one line, so the output is 1 2|.

Show the answer

setw applies to one value only, so put it before each number:

#include <iomanip>
#include <iostream>
 
int main() {
    std::cout << std::setw(4) << 1 << std::setw(4) << 2 << "|\n";
}

The output is 1 2|. Writing std::cout << std::setw(4) << 1 << 2 would give 12, because the second number would have no width.

Summary

  • A format string uses {} holes. std::format fills each hole with the next value and returns a string.
  • A rule after the colon sets alignment, width, precision and the number base. For example {:>8.2f} means right-aligned, width 8, two digits after the point.
  • A rule can read its width from a value with a nested hole, as in {:<{}}.
  • std::setw applies to the next value only. The other iostream manipulators stay on until you change them.
  • printf uses percent codes and does not check types, so a mismatched code gives a warning and a wrong value.
  • Decimal values such as 2.675 can round differently from what you expect, because they are stored in binary.
Check yourself: Formatting (6 questions) →Practice this topic: coding problems for Character Arrays and Strings →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Longest common prefix