Pada Chapter Manipulator kita merapikan kolom dengan setw, setfill, dan setprecision, tetapi setiap pengaturan itu tersebar di tengah rantai << dan sebagian bersifat menempel sehingga mudah terlewat untuk dikembalikan. C++20 dan C++23 menawarkan jalur berbeda: seluruh instruksi tata letak dikumpulkan ke dalam satu string seperti {:.2f}, lalu std::format atau std::print menerapkannya sekaligus. Chapter Formatting Output sudah mencicipi gaya ini; sekarang kita bedah aturan lengkapnya, dari format spec sampai formatter custom.

Format Spec Mini-Language #

Setiap pasangan {} di dalam format string disebut replacement field. Antara tanda : dan penutup } kita menulis format spec — aturan mini yang mengatur lebar, perataan, pengisi, tanda, dan bentuk angka, dengan pola {[[fill]align][sign][#][0][width][.precision][type]}. Satu aturannya penting sejak awal: karakter fill tidak boleh berdiri sendiri dan selalu berpasangan dengan arah perataan, sedangkan align tanpa fill tetap sah dengan pengisi spasi.

cpp
#include <format>
#include <print>
#include <string>

int main() {
    int stok = 42;
    double harga = 1234.5678;
    std::string nama = "Kopi";

    // Width dengan align bawaan: angka rata kanan, string rata kiri
    std::println("[{:10}]", stok);
    std::println("[{:10}]", nama);

    // Fill + align: karakter pertama pengisi, karakter kedua arah rata
    std::println("[{:*^10}]", nama);
    std::println("[{:.<8}]", stok);
    std::println("[{:05}]", stok);

    // Tanda (sign) dan alternate form (#)
    std::println("[{:+}]", stok);
    std::println("[{:#x}] [{:#b}]", 255, 5);

    // Precision untuk floating-point
    std::println("[{:.2f}]", harga);
    std::println("[{:.3e}]", harga);
    std::println("[{}]", harga);

    // Precision pada string dan type 'c' untuk karakter
    std::println("[{:.3}]", nama);
    std::println("[{:c}]", 65);
    return 0;
}

Output yang Dihasilkan:

text
[        42]
[Kopi      ]
[***Kopi***]
[42......]
[00042]
[+42]
[0xff] [0b101]
[1234.57]
[1.235e+03]
[1234.5678]
[Kop]
[A]

Dua perilaku bawaan terlihat jelas di output: angka diratakan kanan sedangkan string diratakan kiri, dan tanpa type floating-point dicetak sebagai representasi terpendek yang tetap tepat secara nilai (shortest round-trip) — berbeda dari cout yang bawaannya memakai enam digit signifikan. Tabel berikut meringkas kombinasi yang dipakai program tersebut:

SpecArtiHasil
{:10}width 10; angka rata kanan, string rata kiri[ 42]
{:*^10}fill * + align tengah[***Kopi***]
{:.<8}fill . + align kanan[42......]
{:05}width 5, diisi nol[00042]
{:+}tanda + selalu tampil[+42]
{:#x} / {:#b}alternate form: awalan 0x / 0b[0xff], [0b101]
{:.2f} / {:.3e}precision: fixed 2 desimal, ilmiah 3 digit[1234.57], [1.235e+03]
{:.3}precision memotong string[Kop]
{:c}type c: cetak int sebagai karakter[A]

Untuk type, integer memakai d (desimal, default), x/X, o, b, dan c; floating-point memakai f (fixed), e (ilmiah), dan g (umum). Precision pada string berarti panjang maksimum — Kopi terpotong menjadi Kop pada baris 29 — dan string punya type sendiri: s untuk tampilan biasa, ? untuk versi ter-escape.

Argumen Posisi dan Lebar Dinamis #

Secara bawaan {} mengisi argumen berurutan. Kita bisa mengambil alih urutan itu dengan arg-id — angka seperti {0} dan {1} di dalam kurung kurawal — sehingga satu argumen boleh dipakai berulang dan urutan penyajian boleh dibalik, misalnya saat pesan diterjemahkan ke bahasa lain yang urutan katanya berbeda.

cpp
#include <format>
#include <print>
#include <string>

int main() {
    // arg-id eksplisit: mengulang dan mengubah urutan argumen
    std::println("{1} vs {0}", "kalah", "menang");
    std::println("{0:x} = {0}", 255);
    std::println("{0} + {1} = {2}", 7, 5, 7 + 5);

    // nested replacement field: lebar/presisi datang dari argumen lain
    std::println("{:*^{}}", "LAPORAN", 16);
    std::println("{:.{}f}", 3.14159, 2);

    // std::format mengembalikan string yang siap disimpan
    std::string kolom = std::format("{0:>8}|{0:<8}|", 42);
    std::println("{}", kolom);
    return 0;
}

Output yang Dihasilkan:

text
menang vs kalah
ff = 255
7 + 5 = 12
****LAPORAN*****
3.14
      42|42      |

Baris demi baris yang perlu kita amati:

  1. Baris 7 ({1} vs {0}): argumen kedua disajikan lebih dulu daripada yang pertama.
  2. Baris 8 ({0:x} = {0}): satu nilai 255 dipakai dua kali dengan spec berbeda.
  3. Baris 9: argumen boleh berupa ekspresi seperti 7 + 5 yang dievaluasi lebih dulu.
  4. Baris 12-13 (nested replacement field): lebar dan presisi datang dari argumen lain — {} di dalam spec mengambil nilai argumen berikutnya.
  5. Baris 16-17: std::format tidak mencetak apa pun, melainkan mengembalikan std::string jadi yang siap disimpan.

Python dan pustaka {fmt} juga mengenal named argument — field yang diisi berdasarkan nama. Standar C++20/23 tidak menyediakannya: grammar replacement field hanya menerima arg-id angka, dan penulisan {nama} ditolak compiler. Alternatif yang sah di dalam standar: indeks posisi, penyusunan string per potongan, atau formatter custom seperti pada bagian akhir chapter ini. Sebagai pembanding, pustaka pihak ketiga {fmt} menyediakan bentuk tersebut:

cpp Gaya named argument pada pustaka fmt (ekstensi, bukan bagian std)
// Pustaka {fmt} mendukung named argument; std::format C++23 belum.
fmt::format("{nama} membeli {jumlah}",
            fmt::arg("nama", "Budi"), fmt::arg("jumlah", 7));

Contoh di atas memakai pustaka pihak ketiga yang tidak terinstal di lingkungan kita, sehingga ditampilkan tanpa klaim output. Satu aturan lagi: pengisian otomatis ({}) tidak boleh dicampur arg-id manual ({0}) dalam satu format string; pemeriksa format string menolaknya saat kompilasi.

Membangun String dengan std::format_to #

Ketiganya punya pembagian kerja: std::format mengembalikan std::string jadi, std::print dan std::println menulis ke stdout, sedangkan std::format_to menulis ke output iterator yang kita tentukan. Dipadukan dengan std::back_inserter (section Iterator dan Algorithms), kita bisa menyusun laporan baris demi baris di dalam loop tanpa penyambungan string berulang-ulang.

cpp
#include <format>
#include <iterator>
#include <print>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::pair<std::string, int>> stok = {
        {"Kopi Toraja", 12},
        {"Teh Hijau", 105},
        {"Gula Pasir", 7},
    };

    std::string laporan;
    auto masuk = std::back_inserter(laporan);
    std::format_to(masuk, "{}\n", "LAPORAN STOK");
    std::format_to(masuk, "{:-^26}\n", "");
    for (const auto& [nama, jumlah] : stok) {
        std::format_to(masuk, "{:<14}{:>6}\n", nama, jumlah);
    }
    std::print("{}", laporan);

    char kode[8];
    auto hasil = std::format_to_n(kode, 7, "{:010}", 1234567);
    *hasil.out = '\0';
    std::println("kode = {} (butuh {} karakter)", kode, hasil.size);
    return 0;
}

Output yang Dihasilkan:

text
LAPORAN STOK
--------------------------
Kopi Toraja       12
Teh Hijau        105
Gula Pasir         7
kode = 0001234 (butuh 10 karakter)

Bedah program laporan di atas:

  1. Baris 15-16: laporan mulai sebagai string kosong, dan penulisan lewat masuk selalu menambah teks ke akhirnya.
  2. Baris 17-18: trik {:-^26} pada string kosong mencetak 26 tanda hubung sebagai garis pemisah.
  3. Baris 19-21: setiap iterasi menulis dua kolom sejajar ({:<14}{:>6}) dan menumpuk hasilnya di laporan.
  4. Baris 22: laporan dicetak sekali setelah tersusun, bukan per iterasi.
  5. Baris 24-26: std::format_to_n menulis maksimal 7 karakter ke buffer char kode[8], lalu kita tutup dengan '\0'; hasil.size melaporkan panjang penuh (10) sehingga terpotong dengan aman.

Formatter Custom untuk Tipe Sendiri #

Compiler tahu cara memformat int dan std::string, tetapi tidak tahu mencetak struct buatan kita — std::println("{}", sunset) ditolak sebelum kita memberi tahu caranya. Solusinya adalah spesialisasi std::formatter. Bentuk template <> struct std::formatter<Warna> adalah template specialization yang dibahas tuntas pada section Generic Programming dan Templates; di sini kita pakai polanya apa adanya. Kata constexpr menandai fungsi yang boleh dievaluasi saat kompilasi, syarat agar pemeriksa format string dapat menjalankan parse() pada waktu kompilasi.

cpp
#include <format>
#include <print>
#include <string>

struct Warna {
    int merah;
    int hijau;
    int biru;
};

template <>
struct std::formatter<Warna> {
    constexpr auto parse(std::format_parse_context& ctx) {
        auto pos = ctx.begin();
        while (pos != ctx.end() && *pos != '}') {
            ++pos;  // versi dasar: isi spec diloloskan tanpa ditafsirkan
        }
        return pos;
    }

    auto format(const Warna& w, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "rgb({}, {}, {})",
                              w.merah, w.hijau, w.biru);
    }
};

int main() {
    Warna sunset{230, 126, 34};
    Warna laut{31, 97, 141};

    std::println("Utama: {}", sunset);
    std::println("Laut  : {}", laut);

    std::string css = std::format("color: {};", sunset);
    std::println("CSS: {}", css);

    std::println("[{:>30}]", sunset);  // spec diabaikan versi dasar ini
    return 0;
}

Output yang Dihasilkan:

text
Utama: rgb(230, 126, 34)
Laut  : rgb(31, 97, 141)
CSS: color: rgb(230, 126, 34);
[rgb(230, 126, 34)]

Dua member wajib ada pada setiap formatter:

  1. Baris 13-19 (parse): menerima konteks yang memuat isi spec dan wajib mengembalikan iterator yang menunjuk ke }; versi dasar ini melompati isi spec tanpa menafsirkannya.
  2. Baris 21-24 (format): menulis lewat std::format_to(ctx.out(), ...) — ctx.out() adalah posisi penulisan berikutnya — lalu mengembalikan iterator hasilnya; kata const wajib karena formatter dipanggil lewat referensi konstan.
  3. Baris 31-35: setelah spesialisasi ada, Warna dipakai di semua fungsi keluarga format tanpa kode tambahan.
  4. Baris 37: spec {:>30} lolos kompilasi tetapi outputnya tidak diratakan — formatter versi dasar ini mengabaikan isi spec.

std::println dan Keluarga Fungsi format #

C++23 menambahkan std::print dan std::println di header <print>; keduanya memformat seperti std::format lalu menulis hasilnya ke stdout. Bedanya hanya di akhir: println menambahkan \n otomatis, dan println() tanpa argumen mencetak satu baris kosong. Keduanya punya overload yang menerima stream tujuan, misalnya std::println(std::cout, ...).

FungsiPerilaku
std::format(fmt, args...)mengembalikan std::string hasil format
std::print(fmt, args...)menulis ke stdout tanpa newline otomatis
std::println(fmt, args...)seperti print, ditambah \n
std::println()tanpa argumen: satu baris kosong
std::println(std::cout, fmt, args...)menulis ke stream yang ditunjuk

Dua catatan teknis: print dan println menulis ke stdout tanpa mengubah flag stream milik cout, sehingga keduanya bisa bercampur dengan rantai << tanpa saling menggeser pengaturan. Lalu, format string harus berupa konstanta waktu kompilasi; string yang disusun saat program berjalan ditolak compiler, dan untuk kasus itu tersedia std::vformat yang memeriksa spec pada runtime.

Kapan format Lebih Tepat daripada iostream #

Keputusan praktisnya begini: std::format mengumpulkan format di satu tempat yang mudah dibaca, memeriksa tipe dan jumlah argumen saat kompilasi, dan menghasilkan string yang bisa disimpan atau dikirim ulang. cout tetap menjadi pilihan untuk membaca input dan untuk kode lama yang berjalan dengan flag stream, sementara tipe custom bisa diberi jalur cetak lewat formatter custom maupun operator<< pada section Pemrograman Berorientasi Objek.

KebutuhanPilihan yang lebih tepatAlasan singkat
Pesan CLI dengan angka rapistd::print / std::printlnformat terkumpul dalam satu string
String siap simpan atau kirimstd::formathasil berupa std::string
Laporan disusun bertahap di loopstd::format_tomenulis ke akhir buffer yang sama
Tipe custom yang sering dicetakformatter customsekali ditulis, dipakai di semua fungsi format
Membaca input penggunaiostream (cin)keluarga format hanya melayani output
Kode lama berbasis flag streamiostreamtidak perlu menulis ulang

Dengan formatter custom, output kini rapi untuk tipe apa pun yang kita buat. Namun, output serapi apa pun percuma bila datanya belum valid — cin yang diminta angka lalu diberi huruf akan gagal membaca. Chapter berikutnya, Input Validation, membahas cara menjaga cin tetap sehat saat input tidak sesuai harapan.

Referensi #