Cara membebani std :: swap ()

115

std::swap()digunakan oleh banyak kontainer std (seperti std::listdan std::vector) selama penyortiran dan bahkan penugasan.

Tetapi implementasi std swap()sangat umum dan agak tidak efisien untuk tipe kustom.

Dengan demikian, efisiensi dapat diperoleh dengan membebani std::swap()dengan implementasi khusus tipe kustom. Tetapi bagaimana Anda bisa menerapkannya sehingga akan digunakan oleh kontainer std?

Adam
sumber
The Swappable halaman pindah ke en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton

Jawaban:

135

Cara yang benar untuk membebani swap adalah dengan menuliskannya di namespace yang sama dengan apa yang Anda tukar, sehingga dapat ditemukan melalui pencarian yang bergantung pada argumen (ADL) . Satu hal yang sangat mudah dilakukan adalah:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};
Dave Abrahams
sumber
11
Dalam C ++ 2003 itu paling tidak ditentukan. Sebagian besar implementasi menggunakan ADL untuk menemukan swap, tetapi tidak, itu tidak diamanatkan, jadi Anda tidak dapat mengandalkannya. Anda dapat mengkhususkan std :: swap untuk jenis beton tertentu seperti yang ditunjukkan oleh OP; hanya saja jangan berharap bahwa spesialisasi akan digunakan, misalnya untuk kelas turunan dari tipe tersebut.
Dave Abrahams
15
Saya akan terkejut menemukan bahwa implementasi masih tidak menggunakan ADL untuk menemukan swap yang benar. Ini adalah masalah lama di komite. Jika implementasi Anda tidak menggunakan ADL untuk menemukan swap, ajukan laporan bug.
Howard Hinnant
3
@Ascha: Pertama, saya mendefinisikan fungsi pada lingkup namespace karena itulah satu-satunya jenis definisi yang penting bagi kode generik. Karena int et. Al. jangan / tidak bisa memiliki fungsi anggota, std :: sort et. Al. harus menggunakan fungsi gratis; mereka menetapkan protokol. Kedua, saya tidak tahu mengapa Anda keberatan memiliki dua implementasi, tetapi sebagian besar kelas akan diurutkan secara tidak efisien jika Anda tidak dapat menerima pertukaran non-anggota. Aturan overloading memastikan bahwa jika kedua deklarasi terlihat, yang lebih spesifik (yang ini) akan dipilih saat swap dipanggil tanpa kualifikasi.
Dave Abrahams
5
@ Mozza314: Tergantung. A std::sortyang menggunakan ADL untuk menukar elemen tidak sesuai dengan C ++ 03 tetapi sesuai dengan C ++ 11. Juga, mengapa -1 jawaban berdasarkan fakta bahwa klien mungkin menggunakan kode non-idiomatik?
JoeG
4
@curiousguy: Jika membaca standar hanyalah masalah sederhana membaca standar, Anda benar :-). Sayangnya, maksud penulis itu penting. Jadi jika maksud aslinya adalah bahwa ADL dapat atau harus digunakan, itu tidak ditentukan. Jika tidak, maka itu hanya perubahan lama biasa untuk C ++ 0x, itulah sebabnya saya menulis "paling banter" di bawah spesifikasi.
Dave Abrahams
70

Perhatian Mozza314

Berikut adalah simulasi dari efek std::algorithmpemanggilan generik std::swap, dan meminta pengguna menyediakan swap mereka di namespace std. Karena ini adalah sebuah eksperimen, simulasi ini menggunakan namespace expbukan namespace std.

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Bagi saya ini cetakannya:

generic exp::swap

Jika kompilator Anda mencetak sesuatu yang berbeda maka kompilator tidak menerapkan "pencarian dua fase" untuk template dengan benar.

Jika kompiler Anda sesuai (ke salah satu dari C ++ 98/03/11), maka itu akan memberikan keluaran yang sama yang saya tunjukkan. Dan dalam kasus seperti itu, apa yang Anda takuti akan terjadi, benar-benar terjadi. Dan memasukkan Anda swapke dalam namespace std( exp) tidak menghentikannya terjadi.

Dave dan saya adalah anggota komite dan telah mengerjakan bidang standar ini selama satu dekade (dan tidak selalu sesuai satu sama lain). Tetapi masalah ini telah diselesaikan untuk waktu yang lama, dan kami berdua sepakat tentang cara menyelesaikannya. Abaikan pendapat / jawaban ahli Dave di bidang ini atas risiko Anda sendiri.

Masalah ini terungkap setelah C ++ 98 diterbitkan. Mulai sekitar 2001 Dave dan saya mulai mengerjakan bidang ini . Dan inilah solusi modern:

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Outputnya adalah:

swap(A, A)

Memperbarui

Pengamatan telah dilakukan bahwa:

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

berhasil! Jadi mengapa tidak menggunakan itu?

Pertimbangkan kasus Anda Aadalah template kelas:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

Sekarang tidak berfungsi lagi. :-(

Jadi Anda bisa memasukkan swapnamespace std dan membuatnya berfungsi. Tapi Anda harus ingat untuk menempatkan swapdiA 's namespace untuk kasus ketika Anda memiliki template: A<T>. Dan karena kedua kasus akan bekerja jika Anda meletakkan swapdi A's namespace, itu hanya lebih mudah untuk mengingat (dan mengajar orang lain) untuk hanya melakukannya bahwa salah satu cara.

Howard Hinnant
sumber
4
Terima kasih banyak atas jawaban mendetailnya. Saya jelas kurang memiliki pengetahuan tentang ini dan sebenarnya bertanya-tanya bagaimana kelebihan beban dan spesialisasi dapat menghasilkan perilaku yang berbeda. Namun, saya tidak menyarankan kelebihan beban tetapi spesialisasi. Ketika saya memasukkan template <>contoh pertama Anda, saya mendapatkan output exp::swap(A, A)dari gcc. Jadi, mengapa tidak lebih memilih spesialisasi?
voltrevo
1
Wow! Ini benar-benar mencerahkan. Anda pasti telah meyakinkan saya. Saya rasa saya akan sedikit memodifikasi saran Anda dan menggunakan sintaks teman di kelas dari Dave Abrahams (hei, saya juga dapat menggunakan ini untuk operator <<! :-)), kecuali Anda juga memiliki alasan untuk menghindarinya (selain menyusun terpisah). Selain itu, menurut Anda, apakah menurut Anda using std::swapmerupakan pengecualian untuk aturan "jangan pernah meletakkan pernyataan menggunakan di dalam file header"? Sebenarnya, kenapa tidak dimasukkan ke using std::swapdalam <algorithm>? Saya kira itu bisa mematahkan sebagian kecil kode orang. Mungkin menolak dukungan dan akhirnya memasukkannya?
voltrevo
3
sintaks teman di kelas seharusnya baik-baik saja. Saya akan mencoba membatasi using std::swapruang lingkup fungsi dalam header Anda. Ya, swaphampir menjadi kata kunci. Tapi tidak, ini bukan kata kunci yang tepat. Jadi sebaiknya jangan mengekspornya ke semua namespace sampai Anda benar-benar harus melakukannya. swapsangat mirip operator==. Perbedaan terbesar adalah bahwa tidak ada yang pernah berpikir untuk menelepon operator==dengan sintaks namespace yang memenuhi syarat (itu akan terlalu jelek).
Howard Hinnant
15
@NielKirk: Apa yang Anda lihat sebagai komplikasi adalah terlalu banyak jawaban yang salah. Tidak ada yang rumit tentang jawaban benar Dave Abrahams: "Cara yang benar untuk membebani swap adalah dengan menuliskannya di namespace yang sama dengan apa yang Anda tukar, sehingga dapat ditemukan melalui pencarian yang bergantung pada argumen (ADL)."
Howard Hinnant
2
@codeshot: Maaf. Herb telah mencoba menyampaikan pesan ini sejak 1998: gotw.ca/publications/mill02.htm Dia tidak menyebutkan swap dalam artikel ini. Tapi ini hanyalah aplikasi lain dari Prinsip Antarmuka Herb.
Howard Hinnant
53

Anda tidak diizinkan (oleh standar C ++) untuk membebani std :: swap, namun Anda secara khusus diizinkan untuk menambahkan spesialisasi template untuk tipe Anda sendiri ke namespace std. Misalnya

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

maka penggunaan di std containers (dan di mana pun) akan memilih spesialisasi Anda, bukan yang umum.

Perhatikan juga bahwa menyediakan implementasi kelas dasar swap tidak cukup baik untuk tipe turunan Anda. Misalnya jika sudah

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

ini akan berfungsi untuk kelas Basis, tetapi jika Anda mencoba menukar dua objek Turunan, ini akan menggunakan versi generik dari std karena swap templated sama persis (dan ini menghindari masalah hanya menukar bagian 'dasar' dari objek turunan Anda ).

CATATAN: Saya telah memperbarui ini untuk menghapus bit yang salah dari jawaban terakhir saya. D'oh! (terima kasih puetzk dan j_random_hacker untuk menunjukkannya)

Wilka
sumber
1
Saran yang paling bagus, tapi saya harus -1 karena perbedaan halus yang dicatat oleh puetzk antara mengkhususkan template di namespace std (yang diizinkan oleh standar C ++) dan overloading (yang tidak).
j_random_hacker
11
Diturunkan karena cara yang benar untuk menyesuaikan swap adalah melakukannya di namespace Anda sendiri (seperti yang ditunjukkan Dave Abrahams dalam jawaban lain).
Howard Hinnant
2
Alasan saya untuk downvoting sama dengan alasan Howard
Dave Abrahams
13
@ HowardHinnant, Dave Abrahams: Saya tidak setuju. Atas dasar apa Anda mengklaim alternatif Anda adalah cara yang "benar"? Seperti yang dikutip puetzk dari standar, ini secara khusus diperbolehkan. Meskipun saya baru dalam masalah ini, saya sangat tidak menyukai metode yang Anda anjurkan karena jika saya mendefinisikan Foo dan menukar seperti itu, orang lain yang menggunakan kode saya kemungkinan besar akan menggunakan std :: swap (a, b) daripada swap ( a, b) di Foo, yang secara diam-diam menggunakan versi default yang tidak efisien.
voltrevo
5
@ Mozza314: Batasan spasi dan pemformatan area komentar tidak memungkinkan saya membalas Anda sepenuhnya. Silakan lihat jawaban yang saya tambahkan berjudul "Attention Mozza314".
Howard Hinnant
29

Meskipun benar bahwa seseorang seharusnya tidak menambahkan barang ke std :: namespace, menambahkan spesialisasi template untuk tipe yang ditentukan pengguna secara khusus diperbolehkan. Membebani fungsi tidak. Ini adalah perbedaan halus :-)

17.4.3.1/1 Tidak ditentukan untuk program C ++ untuk menambahkan deklarasi atau definisi ke namespace std atau namespace dengan namespace std kecuali ditentukan lain. Sebuah program dapat menambahkan spesialisasi template untuk template perpustakaan standar ke namespace std. Spesialisasi seperti itu (lengkap atau sebagian) dari pustaka standar menghasilkan perilaku yang tidak ditentukan kecuali jika deklarasi bergantung pada nama yang ditentukan pengguna dari tautan eksternal dan kecuali spesialisasi templat memenuhi persyaratan pustaka standar untuk templat asli.

Spesialisasi dari std :: swap akan terlihat seperti ini:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

Tanpa template <> bit itu akan menjadi overload, yang tidak terdefinisi, bukan spesialisasi, yang diijinkan. @ Wilka menyarankan pendekatan untuk mengubah namespace default dapat bekerja dengan kode pengguna (karena pencarian Koenig lebih memilih versi tanpa namespace) tetapi itu tidak dijamin, dan pada kenyataannya tidak benar-benar seharusnya (implementasi STL harus menggunakan sepenuhnya -qualified std :: swap).

Ada utas di comp.lang.c ++. Yang dimoderasi dengan diskusi panjang tentang topik. Sebagian besar tentang spesialisasi parsial, meskipun (yang saat ini tidak ada cara yang baik untuk melakukannya).

puetzk.dll
sumber
7
Salah satu alasan salah menggunakan spesialisasi template fungsi untuk ini (atau apa pun): ini berinteraksi dengan cara yang buruk dengan kelebihan beban, yang banyak di antaranya untuk swap. Misalnya, jika Anda mengkhususkan diri pada std :: swap biasa untuk std :: vector <mytype> &, spesialisasi Anda tidak akan dipilih daripada swap khusus vektor standar, karena spesialisasi tidak dipertimbangkan selama resolusi kelebihan beban.
Dave Abrahams
4
Ini juga yang direkomendasikan Meyers dalam Effective C ++ 3ed (Item 25, pp 106-112).
jww
2
@DaveAbrahams: Jika Anda mengkhususkan diri (tanpa argumen template yang eksplisit), pemesanan parsial akan menyebabkan itu menjadi spesialisasi dari yang vectorversi dan akan digunakan .
Davis Herring
1
@DavisHerring sebenarnya, tidak, ketika Anda melakukan pemesanan parsial itu tidak berperan. Masalahnya bukanlah bahwa Anda tidak dapat menyebutnya sama sekali; itulah yang terjadi dengan adanya overload swap yang tampaknya kurang spesifik: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams
2
@DaveAbrahams: Urutan parsial adalah untuk memilih template fungsi yang akan dikhususkan saat spesialisasi eksplisit cocok dengan lebih dari satu. The ::swapkelebihan Anda menambahkan lebih khusus daripada std::swapyang berlebihan untuk vector, sehingga menangkap panggilan dan tidak ada spesialisasi yang terakhir relevan. Saya tidak yakin bagaimana itu masalah praktis (tetapi saya juga tidak mengklaim bahwa ini adalah ide yang bagus!).
Davis Herring