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.
#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:
[ 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:
| Spec | Arti | Hasil |
|---|---|---|
{: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.
#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:
menang vs kalah
ff = 255
7 + 5 = 12
****LAPORAN*****
3.14
42|42 |Baris demi baris yang perlu kita amati:
- Baris 7 (
{1} vs {0}): argumen kedua disajikan lebih dulu daripada yang pertama. - Baris 8 (
{0:x} = {0}): satu nilai255dipakai dua kali dengan spec berbeda. - Baris 9: argumen boleh berupa ekspresi seperti
7 + 5yang dievaluasi lebih dulu. - Baris 12-13 (nested replacement field): lebar dan presisi datang dari argumen lain —
{}di dalam spec mengambil nilai argumen berikutnya. - Baris 16-17:
std::formattidak mencetak apa pun, melainkan mengembalikanstd::stringjadi 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:
// 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.
#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:
LAPORAN STOK
--------------------------
Kopi Toraja 12
Teh Hijau 105
Gula Pasir 7
kode = 0001234 (butuh 10 karakter)Bedah program laporan di atas:
- Baris 15-16:
laporanmulai sebagai string kosong, dan penulisan lewatmasukselalu menambah teks ke akhirnya. - Baris 17-18: trik
{:-^26}pada string kosong mencetak 26 tanda hubung sebagai garis pemisah. - Baris 19-21: setiap iterasi menulis dua kolom sejajar (
{:<14}{:>6}) dan menumpuk hasilnya dilaporan. - Baris 22:
laporandicetak sekali setelah tersusun, bukan per iterasi. - Baris 24-26:
std::format_to_nmenulis maksimal 7 karakter ke bufferchar kode[8], lalu kita tutup dengan'\0';hasil.sizemelaporkan 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.
#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:
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:
- 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. - Baris 21-24 (
format): menulis lewatstd::format_to(ctx.out(), ...)—ctx.out()adalah posisi penulisan berikutnya — lalu mengembalikan iterator hasilnya; kataconstwajib karena formatter dipanggil lewat referensi konstan. - Baris 31-35: setelah spesialisasi ada,
Warnadipakai di semua fungsi keluarga format tanpa kode tambahan. - 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, ...).
| Fungsi | Perilaku |
|---|---|
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.
| Kebutuhan | Pilihan yang lebih tepat | Alasan singkat |
|---|---|---|
| Pesan CLI dengan angka rapi | std::print / std::println | format terkumpul dalam satu string |
| String siap simpan atau kirim | std::format | hasil berupa std::string |
| Laporan disusun bertahap di loop | std::format_to | menulis ke akhir buffer yang sama |
| Tipe custom yang sering dicetak | formatter custom | sekali ditulis, dipakai di semua fungsi format |
| Membaca input pengguna | iostream (cin) | keluarga format hanya melayani output |
| Kode lama berbasis flag stream | iostream | tidak 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.