# tabulate prints styled tables from plain C++ rows

> A header-only C++ library that turns rows of values into bordered, styled tables on a terminal, where formatting resolves cell, then row, then table, and `Table.shape()` reports rendered size rather than row and column counts.

**p-ranav/tabulate** — Table Maker for Modern C++

- Repository: https://github.com/p-ranav/tabulate
- Stars: 2,177 · Forks: 160
- Language: C++
- License: MIT
- Published: 2026-10-13 · Updated: 2026-10-13 · Language: en
- Canonical page: https://hysenlabs.com/projects/p-ranav-tabulate

## Turning plain C++ values into a padded console table

A tabulate table is built from ordinary C++ values and printed straight to a stream, which is why the library turns up in command line tools rather than in desktop software. Two example tables set the target. One lists physical constants whose cells carry the typography of SI units, including Ω, × 10⁻¹²F·m⁻¹ and 6.6742(10) × 10⁻¹¹m³·kg⁻¹·s⁻². The other is an employee register with an identifier, a name, a department and a pay rate such as 59.6154. Neither table is a document exported at the end of a batch run; both are things a program prints while somebody watches. So the library spends its effort on presentation rather than on data handling, and column widths, border characters, padding and styling are settled for you.

## Style inheritance falls back from cell to row to table

When a cell is rendered, tabulate resolves formatting in a fixed order: cell formatting if it was set, otherwise the formatting of the parent row, otherwise the formatting of the table, otherwise the built-in default. That one rule explains why an entire header row can be colored while a single cell inside it stays a different color. The same hierarchy shows up in the accessors. `Table.format()` returns a `Format` object covering the whole table, `Table[row_index]` returns a `Row`, `Table.column(index)` returns a `Column`, and indexing twice gives one `Cell`. Each of those objects carries its own `format()` with the same fluent methods, so a chain like `.font_color(Color::yellow)` reads the same whether it lands on one cell or on a whole column.

## Getting include/ onto the include path and printing one row

Installation is a directory copy. The project is header-only: add `include/` to your `include_directories`, or vendor the amalgamated header from `single_include/`. There is no package manager step, and the write-up documents no compiler flag and no build step of its own.

Open a source file with the header and a table, then feed it rows:

```cpp
#include <tabulate/table.hpp>
using namespace tabulate;

int main() {

  Table universal_constants;

  universal_constants.add_row({"Quantity", "Value"});
  universal_constants.add_row({"Characteristic impedance of vacuum", "376.730 313 461... Ω"});
```

Rows can also arrive through a stream, which drops the brace initializer when a value is computed. `RowStream` is an object you stream into and hand straight to `add_row`, as the employee table does:

```cpp
  employees.add_row({"Emp. ID", "First Name", "Last Name", "Department / Business Unit", "Pay Rate"});
  employees.add_row(RowStream{} << 101 << "Donald" << "Patrick" << "Finance" << 59.6154);
  employees.add_row(RowStream{} << 102 << "Rachel" << "Williams" << "Marketing and Operational\nLogistics Planning" << 34.9707);
```

Formatting is a chain hanging off the table, and the chain returns the object you started from, so it can continue:

```cpp
  universal_constants.format()
    .font_style({FontStyle::bold})
    .border_top(" ")
    .border_bottom(" ")
    .border_left(" ")
    .border_right(" ")
    .corner(" ");
```

A header is an ordinary row reached by index, so it takes the same chained calls:

```cpp
  universal_constants[0].format()
    .padding_top(1)
    .padding_bottom(1)
    .font_align(FontAlign::center)
    .font_style({FontStyle::underline})
    .font_background_color(Color::red);
```

Output goes through `operator<<`, and `Table.print(stream)` does the same job when a stream is passed explicitly.

## Two ways to ask a table for its size

`Table.size()` counts rows. `Table.dimensions()` returns the `{rows, columns}` pair that were actually added, which is the pair you want when the numbers feed indexing rather than printing:

```cpp
Table table;
table.add_row({"cell"});
auto [rows, cols] = table.dimensions(); // {1, 1}
```

The method that catches people out is `Table.shape()`. It is not a second row and column counter. It renders the table and reports the character width and line count of that rendered text, so a table holding one cell gives the width and height of its printed box rather than `{1, 1}`. Callers budgeting terminal space need this one; callers counting data need `dimensions()`. The two names sit next to each other in the same API and answer questions with the same wording, but their units are cells on one side and printed characters on the other.

## A C++11 support claim under C++17 only examples

The support statement and the examples do not line up. One line says the library supports `>=C++11`; the line right after says the rest of the write-up assumes `C++17`. The snippet that unpacks `Table.dimensions()` uses a structured binding, which is a C++17 construct, so a project pinned to C++11 cannot compile that example as written even though the header itself is usable there. A C++11 user copying the documentation hits the compiler rather than the library. Repository topics list `cpp11`, `cpp14` and `cpp17` as three separate tags, which fits a header written against the older standard while the documentation was produced with a newer compiler in hand.

## What the sample programs add beyond the contents list

The contents list names twelve formatting topics under one parent entry: word wrapping, font alignment, font styles, cell colors, borders and corners, Unicode box drawing, indenting a table, streaming rows, pagination, range-based iteration, nested tables and UTF-8 support. The `samples/` directory is where the detail sits. `samples/pagination.cpp` and `samples/iterators.cpp` match the pagination and iteration headings, `samples/nested_table_colors.cpp` and `samples/nested_table_hidden_border.cpp` cover the nested table cases, and `samples/format_after_print.cpp` points at a formatting call made after output has already gone out. `samples/ansi_escape_width.cpp` exists because a colored cell carries bytes that occupy no visible columns, and `samples/locale_preserved.cpp` and `samples/no_trim.cpp` show trimming and locale choices the headings never mention.

One exporter has no heading at all. `samples/latex_export.cpp` sits beside `samples/markdown_export.cpp` and `samples/asciidoc_export.cpp`, while the contents list names only Markdown and AsciiDoc under its exporter entry.

## Where the documentation stops short, and what upkeep costs

Three gaps are worth knowing before committing. Install guidance stops at adding `include/` to your include path, even though the repository also ships `tabulate.pri`, `tabulateConfig.cmake.in`, `CMakeLists.txt` and a `generate-single-include.bash` script, so a qmake or CMake consumer has no documented wiring to copy. Word wrapping is called automatic, and no wrap width, hyphenation or break setting appears anywhere in the text, leaving long cells to whatever width the table happens to land on. `samples/benchmark.cpp` is present while no timing and no comparison with another table library appears in the write-up, so a performance expectation has nothing to check against.

Two upkeep facts matter. The MIT licence sits beside `LICENSE.optional-lite`, `LICENSE.termcolor` and `LICENSE.variant-lite`, so those bundled components carry their own terms rather than one file settling the whole picture. The last push was on 2026-10-05, the repository is not archived, and v2.1 landed the same day with v2.0 the day before, while v1.5 is dated 2023-02-20.

## Rendering to a terminal versus exporting a document

The nearest alternative to tabulate is a different destination rather than another C++ library. Three exporter samples ship in the repository, and each produces text that a later renderer turns into a document. That reverses the division of labour: the exporter writes cells and delimiters, and something else owns column widths, wrapping and borders, which is exactly the set of decisions tabulate takes on your behalf when it prints to a terminal.

The practical split is where the output lands. A table sent to `std::cout` for someone watching a build or a query wants borders, colors and a rendered height the caller can budget against. A table feeding a wiki page, a documentation pipeline or an expected-output file wants plain deterministic text with no escape sequences, and an exporter delivers that without measuring anything. The same row data can take either route, so the choice belongs to the destination rather than to the data model.

## Conclusion

tabulate belongs in a console binary that already holds the rows and needs them framed, colored and measured, and it costs one header file to keep. It does not fit output another renderer formats, pipelines needing plain deterministic text, or teams on C++11 who copy the C++17 examples. Verify three things first: whether `Table.shape()` is what you meant instead of `Table.dimensions()`, whether your build takes the header in `single_include/` instead of `include/`, and what automatic word wrapping does to your longest cell.

## FAQ

### Is tabulate a header-only C++ library, and how do I include it?

Yes. Add `include/` to your `include_directories`, or take the single header version from `single_include/`. The repository also ships `tabulate.pri` and `tabulateConfig.cmake.in`, but the write-up documents no build-system step.

### Which C++ standard does tabulate need?

The library supports `>=C++11`, while the examples in the write-up assume `C++17` and use a structured binding to unpack `Table.dimensions()`.

### How do I count rows and columns in a tabulate table?

`Table.size()` returns the number of rows and `Table.dimensions()` returns the rows and columns actually added. `Table.shape()` is unrelated: it renders the table and reports the character width and line count of the printed text.

### Can tabulate export markdown, AsciiDoc or LaTeX?

Exporter samples exist for all three under `samples/`, although the contents list names only Markdown and AsciiDoc. The exporters emit text for a later renderer rather than drawing a bordered box.

### Does tabulate wrap long cell text automatically?

Word wrapping happens automatically when cells are printed. No wrap width, hyphenation or break option appears in the visible documentation, so the resulting column width follows the content.

## Sources

- [Issues](https://github.com/p-ranav/tabulate/issues)
- [License: MIT](https://github.com/p-ranav/tabulate/blob/master/LICENSE)
- [p-ranav/tabulate on GitHub](https://github.com/p-ranav/tabulate)
- [README](https://github.com/p-ranav/tabulate/blob/master/README.md)
- [Releases](https://github.com/p-ranav/tabulate/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/p-ranav-tabulate
