Mengapa kompiler C ++ tidak mendefinisikan operator == dan operator! =?

302

Saya penggemar berat membiarkan kompiler melakukan sebanyak mungkin pekerjaan untuk Anda. Saat menulis kelas sederhana kompiler dapat memberi Anda yang berikut ini secara 'gratis':

  • Konstruktor (kosong) default
  • Pembuat salinan
  • Seorang destruktor
  • Operator penugasan ( operator=)

Tapi sepertinya tidak bisa memberi Anda operator perbandingan - seperti operator==atau operator!=. Sebagai contoh:

class foo
{
public:
    std::string str_;
    int n_;
};

foo f1;        // Works
foo f2(f1);    // Works
foo f3;
f3 = f2;       // Works

if (f3 == f2)  // Fails
{ }

if (f3 != f2)  // Fails
{ }

Apakah ada alasan bagus untuk ini? Mengapa melakukan perbandingan anggota-oleh-anggota menjadi masalah? Tentunya jika kelas mengalokasikan memori maka Anda ingin berhati-hati, tetapi untuk kelas sederhana tentunya kompiler dapat melakukan ini untuk Anda?

rampok
sumber
4
Tentu saja, juga destruktor disediakan secara gratis.
Johann Gerell
23
Dalam salah satu pembicaraannya baru-baru ini, Alex Stepanov menunjukkan bahwa itu adalah kesalahan untuk tidak memiliki otomatis default ==, dengan cara yang sama bahwa ada tugas otomatis default ( =) dalam kondisi tertentu. (Argumen tentang pointer tidak konsisten karena logika berlaku untuk =dan ==, dan bukan hanya untuk yang kedua).
alfC
2
@becko Ini adalah satu di seri di A9: youtube.com/watch?v=k-meLQaYP5Y , saya tidak ingat di mana dari pembicaraan. Ada juga proposal yang tampaknya sedang menuju C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC
1
@becko, ini adalah salah satu yang pertama di seri "Pemrograman efisien dengan komponen" atau "Pemrograman Percakapan" keduanya di A9, tersedia di Youtube.
alfC
1
@becko Sebenarnya ada jawaban di bawah ini yang menunjuk ke sudut pandang Alex stackoverflow.com/a/23329089/225186
alfC

Jawaban:

71

Kompiler tidak akan tahu apakah Anda menginginkan perbandingan pointer atau perbandingan (internal) yang mendalam.

Lebih aman untuk tidak mengimplementasikannya dan membiarkan programmer melakukannya sendiri. Kemudian mereka dapat membuat semua asumsi yang mereka sukai.

Tandai Ingram
sumber
292
Masalah itu tidak menghentikannya menghasilkan copy ctor, di mana itu cukup berbahaya.
MSalters
78
Salin konstruktor (dan operator=) umumnya bekerja dalam konteks yang sama dengan operator pembanding - yaitu, ada harapan bahwa setelah Anda melakukan a = b, a == bitu benar. Jelas masuk akal bagi kompiler untuk menyediakan default operator==menggunakan semantik nilai agregat yang sama seperti halnya untuk operator=. Saya menduga paercebal sebenarnya benar di sini karena operator=(dan copy ctor) disediakan semata-mata untuk kompatibilitas C, dan mereka tidak ingin memperburuk situasi.
Pavel Minaev
46
-1. Tentu saja Anda menginginkan perbandingan mendalam, jika programmer menginginkan perbandingan pointer, ia akan menulis (& f1 == & f2)
Viktor Sehr
62
Viktor, saya sarankan Anda memikirkan kembali respons Anda. Jika kelas Foo berisi Bar *, lalu bagaimana kompiler mengetahui apakah Foo :: operator == ingin membandingkan alamat Bar *, atau isi Bar?
Tandai Ingram
46
@ Mark: Jika ini berisi sebuah pointer, membandingkan nilai-nilai pointer itu wajar - jika mengandung sebuah nilai, membandingkan nilai-nilai itu wajar. Dalam keadaan luar biasa, programmer bisa menimpa. Ini seperti bahasa yang mengimplementasikan perbandingan antara int dan pointer-to-int.
Eamon Nerbonne
317

Argumen bahwa jika kompiler dapat memberikan konstruktor salinan default, itu harus dapat memberikan default yang sama operator==()masuk akal. Saya berpikir bahwa alasan untuk keputusan untuk tidak menyediakan default yang dibuat compiler untuk operator ini dapat ditebak oleh apa yang dikatakan Stroustrup tentang konstruktor copy default di "Desain dan Evolusi C ++" (Bagian 11.4.1 - Kontrol Menyalin) :

Saya pribadi menganggap sangat disayangkan bahwa operasi penyalinan didefinisikan secara default dan saya melarang penyalinan objek dari banyak kelas saya. Namun, C ++ mewarisi penugasan default dan menyalin konstruktor dari C, dan mereka sering digunakan.

Jadi alih-alih "mengapa C ++ tidak memiliki default operator==()?", Pertanyaannya seharusnya adalah "mengapa C ++ memiliki tugas default dan menyalin konstruktor?", Dengan jawabannya adalah item-item tersebut dimasukkan dengan enggan oleh Stroustrup untuk kompatibilitas dengan C (mungkin penyebab sebagian besar kutil C ++, tetapi juga mungkin alasan utama popularitas C ++).

Untuk tujuan saya sendiri, dalam IDE saya cuplikan yang saya gunakan untuk kelas baru berisi deklarasi untuk operator penugasan pribadi dan menyalin konstruktor sehingga ketika saya membuat kelas baru saya tidak mendapatkan tugas default dan menyalin operasi - saya harus secara eksplisit menghapus deklarasi operasi-operasi tersebut dari private:bagian jika saya ingin kompiler dapat menghasilkannya untuk saya.

Michael Burr
sumber
29
Jawaban yang bagus. Saya hanya ingin menunjukkan bahwa di C ++ 11, daripada membuat operator penugasan dan menyalin konstruktor pribadi, Anda dapat menghapus mereka sepenuhnya seperti ini: Foo(const Foo&) = delete; // no copy constructordanFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc
9
"Namun, C ++ mewarisi penugasan default dan menyalin konstruktor dari C" Itu tidak menyiratkan mengapa Anda harus membuat SEMUA tipe C ++ dengan cara ini. Mereka seharusnya membatasi ini hanya untuk POD lama, hanya tipe yang sudah ada dalam C, tidak lebih.
lukisan
3
Saya pasti bisa mengerti mengapa C ++ mewarisi perilaku ini struct, tapi saya berharap itu membiarkannya classberperilaku berbeda (dan secara masuk akal). Dalam prosesnya, itu juga akan memberikan perbedaan yang lebih bermakna antara structdan classdi samping akses standar.
jamesdlin
@ jamesdlin Jika Anda menginginkan aturan, menonaktifkan deklarasi implisit dan definisi ctors dan penugasan jika seorang dtor dinyatakan akan masuk akal.
Deduplicator
1
Saya masih tidak melihat ada salahnya membiarkan programmer untuk secara eksplisit memesan kompiler untuk membuat operator==. Pada titik ini hanya sintaksis gula untuk beberapa kode boiler-plate. Jika Anda takut bahwa cara ini programmer dapat mengabaikan beberapa pointer di antara bidang kelas, Anda dapat menambahkan kondisi bahwa itu hanya dapat bekerja pada tipe dan objek primitif yang memiliki operator kesetaraan sendiri. Tidak ada alasan untuk melarang ini sepenuhnya.
NO_NAME
93

Bahkan di C ++ 20, kompiler masih tidak akan secara implisit menghasilkan operator==untuk Anda

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

Tetapi Anda akan mendapatkan kemampuan untuk secara default default == sejak C ++ 20 :

struct foo
{
    std::string str;
    int n;

    // either member form
    bool operator==(foo const&) const = default;
    // ... or friend form
    friend bool operator==(foo const&, foo const&) = default;
};

Defaulting ==dilakukan oleh anggota ==(dengan cara yang sama seperti pembuat salinan standar mengerjakan konstruksi salinan anggota). Aturan baru juga memberikan hubungan yang diharapkan antara ==dan !=. Misalnya, dengan deklarasi di atas, saya dapat menulis keduanya:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

Fitur khusus ini (default operator==dan simetri antara ==dan !=) berasal dari satu proposal yang merupakan bagian dari fitur bahasa yang lebih luas operator<=>.

Anton Savin
sumber
Apakah Anda tahu jika ada pembaruan terbaru tentang ini? Apakah akan tersedia di c ++ 17?
dcmm88
3
@ dcmm88 Sayangnya itu tidak akan tersedia di C ++ 17. Saya sudah memperbarui jawabannya.
Anton Savin
2
Proposal yang dimodifikasi yang memungkinkan hal yang sama (kecuali formulir pendek) akan berada di C ++ 20 :)
Rakete1111
Jadi pada dasarnya Anda harus menentukan = default, untuk hal yang tidak dibuat secara default, bukan? Kedengarannya seperti oxymoron bagi saya ("default eksplisit").
artin
@artin Masuk akal karena menambahkan fitur baru ke bahasa tidak boleh merusak implementasi yang ada. Menambahkan standar perpustakaan baru atau hal-hal baru yang dapat dilakukan oleh kompiler adalah satu hal. Menambahkan fungsi anggota baru di mana mereka tidak ada sebelumnya adalah cerita yang berbeda. Untuk mengamankan proyek Anda dari kesalahan itu akan membutuhkan lebih banyak usaha. Saya pribadi lebih suka flag compiler untuk beralih antara default eksplisit dan implisit. Anda membangun proyek dari standar C ++ yang lebih lama, menggunakan default eksplisit dengan flag compiler. Anda sudah memperbarui kompiler sehingga Anda harus mengonfigurasinya dengan benar. Untuk proyek baru buatlah itu tersirat.
Maciej Załucki
44

IMHO, tidak ada alasan "baik". Alasan ada begitu banyak orang yang setuju dengan keputusan desain ini adalah karena mereka tidak belajar untuk menguasai kekuatan semantik berbasis nilai. Orang-orang perlu menulis banyak konstruktor salinan kustom, operator perbandingan dan destruktor karena mereka menggunakan pointer mentah dalam implementasi mereka.

Saat menggunakan smart pointer yang tepat (seperti std :: shared_ptr), konstruktor salinan default biasanya baik dan implementasi jelas dari operator perbandingan default hipotetis akan sama baiknya.

alexk7
sumber
39

Itu dijawab C ++ tidak melakukan == karena C tidak, dan inilah mengapa C hanya menyediakan default = tetapi tidak ada == pada awalnya. C ingin tetap sederhana: C diimplementasikan = oleh memcpy; Namun, == tidak dapat diterapkan oleh memcmp karena padding. Karena padding tidak diinisialisasi, memcmp mengatakan mereka berbeda walaupun mereka sama. Masalah yang sama ada untuk kelas kosong: memcmp mengatakan mereka berbeda karena ukuran kelas kosong bukan nol. Dapat dilihat dari atas bahwa implementasi == lebih rumit daripada implementasi = dalam C. Beberapa contoh kode mengenai ini. Koreksi Anda dihargai jika saya salah.

Rio Wing
sumber
6
C ++ tidak menggunakan memcpy untuk operator=- yang hanya akan berfungsi untuk tipe POD, tetapi C ++ juga menyediakan default operator=untuk tipe non POD.
Flexo
2
Ya, C ++ diimplementasikan = dengan cara yang lebih canggih. Tampaknya C baru diimplementasikan = dengan memcpy sederhana.
Rio Wing
Isi dari jawaban ini harus disatukan dengan jawaban Michael. Nya mengoreksi pertanyaan maka ini menjawabnya.
Sgene9
27

Dalam video ini Alex Stepanov, pencipta STL menjawab pertanyaan ini sekitar pukul 13:00. Sebagai rangkuman, setelah menyaksikan evolusi C ++ ia berpendapat bahwa:

  • Sangat disayangkan bahwa == dan! = Tidak dinyatakan secara implisit (dan Bjarne setuju dengannya). Bahasa yang benar seharusnya sudah siap untuk Anda (dia melangkah lebih jauh untuk menyarankan Anda seharusnya tidak dapat mendefinisikan ! = Yang memecah semantik dari == )
  • Alasannya adalah kasus ini berakar (karena banyak masalah C ++) di C. Di sana, operator penugasan secara implisit didefinisikan dengan penugasan sedikit demi sedikit tetapi itu tidak akan berhasil untuk == . Penjelasan lebih rinci dapat ditemukan dalam artikel ini dari Bjarne Stroustrup.
  • Dalam pertanyaan tindak lanjut Mengapa kemudian bukan perbandingan anggota dengan anggota digunakan, dia mengatakan hal yang luar biasa : C adalah jenis bahasa yang di-homegrown dan orang yang mengimplementasikan hal-hal ini untuk Ritchie mengatakan kepadanya bahwa dia merasa ini sulit untuk diimplementasikan!

Dia kemudian mengatakan bahwa di masa depan (jauh) == dan ! = Akan dihasilkan secara implisit.

Nikos Athanasiou
sumber
2
Sepertinya masa depan yang jauh ini tidak akan menjadi 2017 atau 18, atau 19, yah Anda tahu
maksud
18

C ++ 20 menyediakan cara untuk dengan mudah mengimplementasikan operator perbandingan default.

Contoh dari cppreference.com :

class Point {
    int x;
    int y;
public:
    auto operator<=>(const Point&) const = default;
    // ... non-comparison functions ...
};

// compiler implicitly declares operator== and all four relational operators work
Point pt1, pt2;
if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
std::set<Point> s; // ok
s.insert(pt1); // ok
if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>
vll
sumber
4
Saya terkejut bahwa mereka digunakan Pointsebagai contoh untuk operasi pemesanan , karena tidak ada cara standar yang masuk akal untuk memesan dua titik dengan xdan ykoordinat ...
pipe
4
@pipe Jika Anda tidak peduli urutan elemen-elemennya, menggunakan operator default masuk akal. Misalnya, Anda dapat menggunakan std::setuntuk memastikan semua poin unik, dan hanya std::setmenggunakan operator<.
vll
Tentang jenis pengembalian auto: Untuk kasus ini, bisakah kita selalu menganggapnya std::strong_orderingdari #include <compare>?
kevinarpe
1
@ kevinarpe Jenis kembali adalah std::common_comparison_category_t, yang untuk kelas ini menjadi pemesanan default ( std::strong_ordering).
vll
15

Tidak mungkin untuk mendefinisikan default ==, tetapi Anda dapat menentukan default !=melalui ==mana Anda biasanya harus mendefinisikan sendiri. Untuk ini, Anda harus melakukan hal-hal berikut:

#include <utility>
using namespace std::rel_ops;
...

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

Anda dapat melihat http://www.cplusplus.com/reference/std/utility/rel_ops/ untuk detailnya.

Selain itu jika Anda mendefinisikan operator< , operator untuk <=,>,> = dapat disimpulkan dari itu ketika menggunakan std::rel_ops.

Tetapi Anda harus berhati-hati ketika menggunakan std::rel_opskarena operator pembanding dapat disimpulkan untuk jenis yang tidak Anda harapkan.

Cara yang lebih disukai untuk menyimpulkan operator terkait dari yang dasar adalah menggunakan boost :: operator .

Pendekatan yang digunakan dalam boost lebih baik karena mendefinisikan penggunaan operator untuk kelas yang Anda inginkan, bukan untuk semua kelas dalam ruang lingkup.

Anda juga dapat menghasilkan "+" dari "+ =", - dari "- =", dll ... (lihat daftar lengkap di sini )

sergtk
sumber
Saya tidak mendapatkan default !=setelah ==operator menulis . Atau saya lakukan tetapi kurang const. Harus menulis sendiri juga dan semuanya baik-baik saja.
John
Anda dapat bermain dengan ketegaran untuk mencapai hasil yang dibutuhkan. Tanpa kode sulit untuk mengatakan apa yang salah dengannya.
sergtk
2
Ada alasan yang rel_opstidak digunakan lagi dalam C ++ 20: karena tidak berfungsi , setidaknya tidak di mana-mana, dan tentu saja tidak secara konsisten. Tidak ada cara yang dapat diandalkan sort_decreasing()untuk mengkompilasi. Di sisi lain, Boost.Operators bekerja dan selalu berfungsi.
Barry
10

C ++ 0x telah memiliki proposal untuk fungsi-fungsi default, sehingga Anda dapat mengatakan default operator==; Kami telah belajar bahwa ini membantu untuk membuat hal-hal ini eksplisit.

MSalters
sumber
3
Saya pikir hanya "fungsi anggota khusus" (konstruktor default, salin konstruktor, operator penugasan, dan destruktor) yang dapat secara eksplisit default. Sudahkah mereka memperluas ini ke beberapa operator lain?
Michael Burr
4
Pindahkan konstruktor juga bisa default, tapi saya rasa ini tidak berlaku operator==. Sangat disayangkan.
Pavel Minaev
5

Secara konseptual tidak mudah untuk mendefinisikan kesetaraan. Bahkan untuk data POD, orang dapat berargumen bahwa bahkan jika bidangnya sama, tetapi itu adalah objek yang berbeda (pada alamat yang berbeda) tidak harus sama. Ini sebenarnya tergantung pada penggunaan operator. Sayangnya kompiler Anda tidak bersifat psikis dan tidak dapat menyimpulkannya.

Selain itu, fungsi default adalah cara terbaik untuk menembak diri sendiri di kaki. Default yang Anda jelaskan pada dasarnya ada untuk menjaga kompatibilitas dengan POD struct. Namun mereka menyebabkan lebih dari cukup kekacauan dengan pengembang lupa tentang mereka, atau semantik dari implementasi standar.

Paul de Vrieze
sumber
10
Tidak ada ambiguitas untuk POD struct - mereka harus berperilaku dengan cara yang persis sama seperti jenis POD lainnya, yaitu kesetaraan nilai (bukan referensi kesetaraan). Satu yang intdibuat melalui copy ctor dari yang lain sama dengan yang dibuatnya; satu-satunya hal logis yang harus dilakukan untuk structdua intbidang adalah bekerja dengan cara yang sama persis.
Pavel Minaev
1
@ mgiuca: Saya bisa melihat kegunaan yang cukup besar untuk hubungan kesetaraan universal yang akan memungkinkan semua jenis yang berperilaku sebagai nilai untuk digunakan sebagai kunci dalam kamus atau koleksi serupa. Namun, koleksi semacam itu tidak dapat berlaku bermanfaat tanpa hubungan ekuivalensi refleksif yang dijamin. IMHO, solusi terbaik adalah dengan mendefinisikan operator baru yang semua tipe bawaan dapat diimplementasikan dengan bijaksana, dan mendefinisikan beberapa tipe pointer baru yang seperti yang ada kecuali bahwa beberapa akan mendefinisikan kesetaraan sebagai kesetaraan referensi sementara yang lain akan rantai ke target. operator kesetaraan.
supercat
1
@supercat Secara analogi, Anda bisa membuat argumen yang hampir sama untuk +operator dalam hal itu non-asosiatif untuk float; itu (x + y) + z! = x + (y + z), karena cara pembulatan FP terjadi. (Bisa dibilang, ini adalah masalah yang jauh lebih buruk daripada ==karena itu benar untuk nilai numerik normal.) Anda mungkin menyarankan menambahkan operator tambahan baru yang bekerja untuk semua jenis numerik (bahkan int) dan hampir persis sama seperti +tetapi asosiatif ( entah bagaimana). Tetapi kemudian Anda akan menambahkan mengasapi dan kebingungan untuk bahasa tanpa benar-benar membantu banyak orang.
mgiuca
1
@ Maggiuca: Memiliki hal-hal yang sangat mirip kecuali pada kasus tepi sering sangat berguna, dan upaya yang salah arah untuk menghindari hal-hal seperti itu menghasilkan banyak kompleksitas yang tidak perlu. Jika kode klien kadang-kadang membutuhkan kasus tepi untuk ditangani satu cara, dan kadang-kadang membutuhkannya untuk ditangani yang lain, memiliki metode untuk setiap gaya penanganan akan menghilangkan banyak kode penanganan kasus tepi di klien. Adapun analogi Anda, tidak ada cara untuk mendefinisikan operasi pada nilai-nilai floating-point berukuran tetap untuk menghasilkan hasil transitif dalam semua kasus (meskipun beberapa bahasa 1980-an memiliki semantik yang lebih baik ...
supercat
1
... daripada hari ini dalam hal ini) dan fakta bahwa mereka tidak melakukan hal yang mustahil seharusnya tidak mengejutkan. Namun, tidak ada kendala mendasar untuk menerapkan hubungan ekivalensi yang secara universal dapat diterapkan pada semua jenis nilai yang dapat disalin.
supercat
1

Apakah ada alasan bagus untuk ini? Mengapa melakukan perbandingan anggota-oleh-anggota menjadi masalah?

Ini mungkin bukan masalah secara fungsional, tetapi dalam hal kinerja, perbandingan anggota-per-anggota standar cenderung lebih sub-optimal daripada penugasan / penyalinan anggota-oleh-anggota standar. Tidak seperti urutan penugasan, urutan perbandingan berdampak pada kinerja karena anggota yang tidak setara pertama menyiratkan yang lainnya dapat dilewati. Jadi, jika ada beberapa anggota yang biasanya sama, Anda ingin membandingkannya terakhir, dan kompilator tidak tahu anggota mana yang lebih mungkin sama.

Pertimbangkan contoh ini, di mana verboseDescriptionstring panjang dipilih dari serangkaian deskripsi cuaca yang relatif kecil.

class LocalWeatherRecord {
    std::string verboseDescription;
    std::tm date;
    bool operator==(const LocalWeatherRecord& other){
        return date==other.date
            && verboseDescription==other.verboseDescription;
    // The above makes a lot more sense than
     // return verboseDescription==other.verboseDescription
     //     && date==other.date;
    // because some verboseDescriptions are liable to be same/similar
    }
}

(Tentu saja kompiler akan berhak mengabaikan urutan perbandingan jika mengakui bahwa mereka tidak memiliki efek samping, tetapi mungkin masih akan mengambil que dari kode sumber di mana ia tidak memiliki informasi yang lebih baik sendiri.)

Museful
sumber
Tetapi tidak ada yang mencegah Anda menulis perbandingan yang dioptimalkan oleh pengguna jika Anda menemukan masalah kinerja. Dalam pengalaman saya itu akan menjadi minoritas kecil sekali.
Peter - Reinstate Monica
1

Hanya agar jawaban atas pertanyaan ini tetap lengkap seiring berjalannya waktu: karena C ++ 20 dapat secara otomatis dihasilkan dengan perintah auto operator<=>(const foo&) const = default;

Ini akan menghasilkan semua operator: ==,! =, <, <=,>, Dan> =, lihat https://en.cppreference.com/w/cpp/language/default_comparisons untuk detailnya.

Karena tampilan operator <=>, itu disebut operator pesawat ruang angkasa. Lihat juga Mengapa kita membutuhkan operator pesawat ruang angkasa <=> di C ++? .

EDIT: juga di C ++ 11 pengganti yang cukup rapi untuk yang tersedia dengan std::tiemelihat https://en.cppreference.com/w/cpp/utility/tuple/tie untuk contoh kode lengkap dengan bool operator<(…). Bagian yang menarik, diubah untuk bekerja dengan ==adalah:

#include <tuple>

struct S {
………
bool operator==(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
    }
};

std::tie bekerja dengan semua operator perbandingan, dan sepenuhnya dioptimalkan oleh kompiler.

cosurgi
sumber
-1

Saya setuju, untuk kelas tipe POD maka kompiler dapat melakukannya untuk Anda. Namun apa yang Anda anggap sederhana kompiler mungkin salah. Jadi lebih baik membiarkan programmer melakukannya.

Saya pernah memiliki kasus POD di mana dua bidang unik - sehingga perbandingan tidak akan pernah dianggap benar. Namun perbandingan yang saya butuhkan hanya pernah dibandingkan pada payload - sesuatu yang kompiler tidak akan pernah mengerti atau pernah bisa mengetahuinya sendiri.

Selain itu - mereka tidak butuh waktu lama untuk menulis, kan ?!

graham.reeds
sumber