Operator konversi dan explicit

Konversi kustom dan pengendalian konversi tak terduga.

Daftar isi

Topik ini membahas operator konversi pada tipe kelas, yaitu fungsi yang mengubah objek menjadi tipe lain, bersama kata kunci explicit yang mengendalikan kapan konversi boleh terjadi. Pembahasan mencakup dua arah konversi, kejutan yang muncul dari konversi implisit, dan cara menutup celahnya. Contohnya memakai kelas Pecahan yang dikonversi menjadi double.

Dua arah konversi tipe buatan sendiri#

Konversi menuju kelas diberikan lewat constructor yang menerima satu parameter, misalnya Pecahan(int) yang mengubah bilangan bulat menjadi pecahan. Konversi dari kelas menuju tipe lain diberikan lewat conversion operator, yaitu fungsi yang mengubah objek menjadi tipe lain. Sintaksnya berbeda dari fungsi biasa: namanya berupa kata kunci operator diikuti nama tipe tujuan, sedangkan tipe baliknya tidak ditulis karena sudah tersirat pada namanya.

Deklarasi lengkapnya berupa operator double() const tanpa parameter. Sifat const penting karena konversi tidak seharusnya mengubah objek sumbernya. Setelah operator ini ada, objek Pecahan dapat muncul di tempat mana pun yang meminta double, mulai dari penugasan, aritmetika, sampai pemanggilan fungsi. Kemudahan inilah yang sekaligus menjadi sumber masalah pada bagian berikutnya.

Sifat constructor berparameter tunggal perlu ditinjau pada waktu yang sama. Constructor Pecahan(int) yang tidak ditandai explicit membuat penugasan seperti Pecahan p = 3; dianggap sah, dan fungsi yang meminta Pecahan dapat menerima int tanpa terlihat di titik pemanggilannya. Kelas Suhu dengan constructor Suhu(double) mengalami hal serupa, karena bilangan apa pun dapat berubah menjadi suhu tanpa peringatan. Dua arah konversi ini menentukan seberapa mudah sebuah tipe berubah bentuk tanpa disadari, sehingga keduanya perlu dikendalikan dengan cara yang sama.

Operator konversi implisit dan kejutannya#

Program berikut mendefinisikan operator double() tanpa explicit, sehingga compiler menerapkannya secara diam-diam. Jalankan programnya dan cermati baris mana saja yang sesungguhnya memicu konversi.

cpp pecahan-konversi.cpp
#include <iostream>

class Pecahan {
public:
    Pecahan(int pembilang, int penyebut) : pembilang(pembilang), penyebut(penyebut) {}

    // operator konversi: Pecahan dapat berubah menjadi double secara implisit
    operator double() const {
        return static_cast<double>(pembilang) / penyebut;
    }

    int getPembilang() const { return pembilang; }
    int getPenyebut() const { return penyebut; }

private:
    int pembilang;
    int penyebut;
};

void tampilkanNilai(double nilai) {
    std::cout << "nilai: " << nilai << "\n";
}

int main() {
    Pecahan p(1, 2);
    Pecahan nol(0, 5);

    double x = p;   // konversi implisit ke double
    std::cout << "x = " << x << "\n";

    std::cout << "p dicetak langsung: " << p << "\n";
    tampilkanNilai(p);

    if (p) {
        std::cout << "p dianggap bernilai bukan nol\n";
    }
    if (!nol) {
        std::cout << "nol dianggap bernilai nol\n";
    }
    return 0;
}
text Keluaran
x = 0.5
p dicetak langsung: 0.5
nilai: 0.5
p dianggap bernilai bukan nol
nol dianggap bernilai nol

Keluarannya memperlihatkan beberapa konteks yang semuanya mengonversi tanpa diminta. Baris pertama memang disengaja, tetapi baris p dicetak langsung: 0.5 memperlihatkan bahwa std::cout memilih mencetak hasil konversi double, bukan bentuk pecahan yang dimaksudkan. Pemanggilan tampilkanNilai(p) juga lolos tanpa peringatan apa pun, dan kondisi if (p) mengubah objek menjadi nilai bool lewat double. Semua konversi ini sah menurut aturan bahasa, sehingga compiler tidak menyampaikan pesan apa pun.

Konversi implisit turut bekerja saat pemilihan overload. Apabila tersedia fungsi proses(int) dan fungsi proses(double), pemanggilan proses(p) memilih versi double tanpa kesempatan menolak. Rantai aritmetika seperti p + 0.25 pun dievaluasi dalam double, sehingga hasilnya bukan lagi Pecahan melainkan bilangan mengambang. Bagi tipe yang maknanya bergantung pada representasinya, seperti pecahan, kehilangan representasi tersebut mudah luput dari pengamatan.

Mengendalikan konversi dengan explicit#

Kata kunci explicit ditempatkan di depan deklarasi operator konversi dan membatasi konversi pada konteks yang memintanya secara langsung. Konteks tersebut mencakup cast bernama seperti static_cast<double>(p) dan inisialisasi langsung (direct-initialization) seperti double y(p). Konteks konversi implisit seperti pemanggilan fungsi dan pencetakan langsung ditolak compiler. Constructor berparameter tunggal dapat diberi explicit dengan cara yang sama untuk mengendalikan arah konversi menuju kelas.

Program berikut menandai operator double() dengan explicit dan menambahkan operator bool() yang juga explicit untuk pengujian keadaan. Baris yang memicu error sengaja ditulis sebagai komentar agar programnya tetap dapat dikompilasi.

cpp konversi-eksplisit.cpp
#include <iostream>

class Pecahan {
public:
    Pecahan(int pembilang, int penyebut) : pembilang(pembilang), penyebut(penyebut) {}

    // explicit: konversi hanya terjadi bila diminta secara langsung
    explicit operator double() const {
        return static_cast<double>(pembilang) / penyebut;
    }

    // explicit operator bool untuk pengujian keadaan di kondisi if
    explicit operator bool() const {
        return pembilang != 0;
    }

    int getPembilang() const { return pembilang; }
    int getPenyebut() const { return penyebut; }

private:
    int pembilang;
    int penyebut;
};

void tampilkanNilai(double nilai) {
    std::cout << "nilai: " << nilai << "\n";
}

int main() {
    Pecahan p(1, 2);

    double x = static_cast<double>(p);   // konversi diminta secara eksplisit
    std::cout << "x = " << x << "\n";

    double y(p);   // direct-initialization tetap mengizinkan konversi explicit
    std::cout << "y = " << y << "\n";

    // std::cout << p;      // error: konversi implisit ke double sudah tidak ada
    // tampilkanNilai(p);   // error: pun demikian

    if (p) {   // kondisi if menerima explicit operator bool lewat konversi kontekstual
        std::cout << "p dianggap bernilai bukan nol\n";
    }
    return 0;
}
text Keluaran
x = 0.5
y = 0.5
p dianggap bernilai bukan nol

Baris std::cout << p yang sebelumnya mencetak 0.5 kini gagal dikompilasi apabila komentarnya dibuka, karena tidak ada lagi konversi implisit yang cocok dengan overload operator<<. Adapun if (p) tetap berhasil karena kondisi if memakai konversi kontekstual (contextual conversion) ke bool. Konversi kontekstual tersebut hanya mempertimbangkan operator yang tipe baliknya persis bool, itulah sebabnya operator double() yang explicit tidak ikut dipakai di kondisi if.

Kombinasi inilah yang menjadikan explicit operator bool pola standar, dan pustaka standar memakainya pada tipe seperti std::unique_ptr. Pesan error yang muncul pun berisi ketidakcocokan konversi yang jelas, sehingga lebih mudah ditindaklanjuti daripada perilaku senyap sebelumnya.

Kesalahan umum#

  • Menulis dua operator konversi ke tipe yang berdekatan, misalnya ke double dan ke float. Compiler menemukan dua kandidat yang sama-sama perlu konversi, sehingga ekspresi tertentu berakhir sebagai error ambiguitas. Cukup satu konversi ke tipe yang paling umum, sisanya ditangani konversi standar.
  • Melupakan const pada operator konversi. Operator konversi tidak mengubah objeknya, sehingga objek const tidak dapat dikonversi dan pemanggilannya gagal. Tambahkan const pada deklarasinya.
  • Membuat operator bool tanpa explicit. Pencetakan langsung seperti std::cout << p memilih overload bool dan mencetak 1 alih-alih isi objek. Tandai dengan explicit agar konversinya hanya dipakai di konteks yang meminta.
  • Mengizinkan konversi menuju kelas tanpa explicit pada constructor berparameter tunggal. Constructor Suhu(double) yang implisit membuat penugasan Suhu s = 36.5; sah, tetapi juga membuat pemanggilan yang salah tipe lolos tanpa peringatan. Tandai dengan explicit bila konversinya tidak diminta secara jelas.

Ringkasan#

  • Constructor berparameter tunggal memberi konversi menuju kelas, sedangkan operator konversi memberi konversi dari kelas ke tipe lain.
  • Konversi implisit diterapkan compiler pada banyak konteks dan dapat mengubah makna program tanpa peringatan.
  • explicit membatasi konversi pada cast bernama dan inisialisasi langsung, sementara explicit operator bool tetap dipakai di kondisi if melalui konversi kontekstual.

Referensi#