วิธีโอเวอร์โหลด std :: swap ()


115

std::swap()ถูกใช้โดยคอนเทนเนอร์มาตรฐานจำนวนมาก (เช่นstd::listและstd::vector) ในระหว่างการเรียงลำดับและแม้กระทั่งการกำหนด

แต่การนำ std ไปใช้swap()นั้นเป็นแบบทั่วไปและค่อนข้างไม่มีประสิทธิภาพสำหรับประเภทที่กำหนดเอง

ดังนั้นประสิทธิภาพจะได้รับจากการโอเวอร์โหลดstd::swap()ด้วยการใช้งานเฉพาะประเภทที่กำหนดเอง แต่คุณจะใช้มันได้อย่างไรเพื่อให้ std container ใช้?


ข้อมูลที่เกี่ยวข้อง: en.cppreference.com/w/cpp/concept/Swappable
Pharap

ถอดเปลี่ยนได้ทันทีหน้าย้ายไปen.cppreference.com/w/cpp/named_req/Swappable
แอนดรู Keeton

คำตอบ:


135

วิธีที่จะแลกเปลี่ยนเกินคือการเขียนมันใน namespace เช่นเดียวกับสิ่งที่คุณกำลังแลกเปลี่ยนเพื่อที่จะสามารถพบได้ผ่านการค้นหาโต้แย้งขึ้นอยู่กับ (ADL) สิ่งหนึ่งที่ทำได้ง่ายโดยเฉพาะคือ:

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);
        // ...
    }
};

11
ใน C ++ 2003 จะมีการระบุขั้นต่ำที่ดีที่สุด การใช้งานส่วนใหญ่ใช้ ADL เพื่อค้นหา swap แต่ไม่ได้รับคำสั่งดังนั้นคุณจึงไม่สามารถวางใจได้ คุณสามารถเชี่ยวชาญ std :: swap สำหรับประเภทคอนกรีตเฉพาะดังที่แสดงโดย OP; อย่าคาดหวังว่าความเชี่ยวชาญนั้นจะถูกนำมาใช้เช่นสำหรับคลาสที่ได้รับจากประเภทนั้น
Dave Abrahams

15
ฉันจะแปลกใจที่พบว่าการใช้งานยังคงไม่ใช้ ADL เพื่อค้นหา swap ที่ถูกต้อง เรื่องนี้เป็นเรื่องเก่าที่คณะกรรมการ หากการใช้งานของคุณไม่ได้ใช้ ADL เพื่อค้นหาการแลกเปลี่ยนให้ยื่นรายงานข้อบกพร่อง
Howard Hinnant

3
@Sascha: อันดับแรกฉันกำลังกำหนดฟังก์ชันที่ขอบเขตเนมสเปซเพราะนั่นเป็นคำจำกัดความประเภทเดียวที่มีความสำคัญกับโค้ดทั่วไป เพราะ int et. อัล ไม่ / ไม่สามารถมีฟังก์ชันสมาชิก std :: sort et. อัล ต้องใช้ฟังก์ชันฟรี พวกเขาสร้างโปรโตคอล ประการที่สองฉันไม่รู้ว่าทำไมคุณถึงคัดค้านการใช้งานสองแบบ แต่คลาสส่วนใหญ่ถึงวาระที่จะถูกจัดเรียงอย่างไม่มีประสิทธิภาพหากคุณไม่สามารถยอมรับการแลกเปลี่ยนที่ไม่ใช่สมาชิกได้ กฎการโอเวอร์โหลดจะช่วยให้มั่นใจได้ว่าหากมีการประกาศทั้งสองรายการจะมีการเลือกคำประกาศที่เฉพาะเจาะจงมากขึ้น (ข้อนี้) เมื่อมีการเรียก swap โดยไม่มีคุณสมบัติ
Dave Abrahams

5
@ Mozza314: ขึ้นอยู่กับ A std::sortที่ใช้ ADL เพื่อสลับองค์ประกอบไม่เป็นไปตาม C ++ 03 แต่สอดคล้องกับ C ++ 11 นอกจากนี้ทำไม -1 คำตอบจากข้อเท็จจริงที่ว่าลูกค้าอาจใช้รหัสที่ไม่ใช่สำนวน?
JoeG

4
@curiousguy: หากการอ่านมาตรฐานเป็นเพียงเรื่องธรรมดาในการอ่านมาตรฐานคุณจะคิดถูก :-) น่าเสียดายที่เจตนาของผู้เขียนมีความสำคัญ ดังนั้นหากเจตนาเดิมคือ ADL สามารถใช้หรือควรใช้ก็ไม่ระบุ ถ้าไม่เช่นนั้นมันเป็นเพียงการเปลี่ยนแปลงแบบเก่า ๆ ธรรมดา ๆ สำหรับ C ++ 0x ซึ่งเป็นเหตุผลว่าทำไมฉันจึงเขียนขีดล่าง "ที่ดีที่สุด"
Dave Abrahams

70

สนใจ Mozza314

นี่คือการจำลองเอฟเฟกต์ของการstd::algorithmโทรทั่วไปstd::swapและการให้ผู้ใช้ทำการแลกเปลี่ยนในเนมสเปซมาตรฐาน เช่นนี้คือการทดลองจำลองนี้ใช้แทนnamespace expnamespace 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);
}

สำหรับฉันสิ่งนี้พิมพ์ออกมา:

generic exp::swap

หากคอมไพลเลอร์ของคุณพิมพ์สิ่งที่แตกต่างออกไปแสดงว่าการใช้ "การค้นหาสองเฟส" สำหรับเทมเพลตไม่ถูกต้อง

หากคอมไพเลอร์ของคุณสอดคล้อง (กับ C ++ 98/03/11 ใด ๆ ) ก็จะให้ผลลัพธ์เดียวกันกับที่ฉันแสดง และในกรณีนี้สิ่งที่คุณกลัวจะเกิดขึ้น และการใส่swapเนมสเปซstd( exp) ของคุณไม่ได้หยุดยั้งไม่ให้เกิดขึ้น

ฉันและเดฟต่างก็เป็นสมาชิกคณะกรรมการและทำงานด้านมาตรฐานนี้มานานกว่าทศวรรษแล้ว (และไม่ใช่ข้อตกลงซึ่งกันและกันเสมอไป) แต่ปัญหานี้ได้รับการแก้ไขเป็นเวลานานแล้วและเราทั้งสองฝ่ายเห็นพ้องกันว่าจะยุติอย่างไร ไม่สนใจความคิดเห็น / คำตอบของผู้เชี่ยวชาญของ Dave ในพื้นที่นี้ด้วยความเสี่ยงของคุณเอง

ปัญหานี้เกิดขึ้นหลังจากเผยแพร่ C ++ 98 เริ่มต้นประมาณ 2,001 เดฟและฉันเริ่มที่จะทำงานในพื้นที่นี้ และนี่คือทางออกที่ทันสมัย:

// 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);
}

ผลลัพธ์คือ:

swap(A, A)

ปรับปรุง

มีการสังเกตว่า:

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

}

ทำงาน! แล้วทำไมไม่ใช้ล่ะ?

พิจารณากรณีที่คุณAเป็นแม่แบบคลาส:

// 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);
}

ตอนนี้ใช้ไม่ได้อีกเลย :-(

ดังนั้นคุณสามารถใส่swapเนมสเปซมาตรฐานและใช้งานได้ แต่คุณจะต้องจำไว้ว่าให้ใส่swapในAnamespace 's A<T>สำหรับกรณีที่เมื่อคุณมีแม่แบบ: และเนื่องจากทั้งสองกรณีจะทำงานถ้าคุณใส่swapในA's namespace ก็เป็นเพียงง่ายต่อการจดจำ (และสอนคนอื่น) ไปเพียงแค่ทำมันว่าวิธีหนึ่ง


4
ขอบคุณมากสำหรับคำตอบโดยละเอียด ฉันมีความรู้น้อยกว่าอย่างชัดเจนเกี่ยวกับเรื่องนี้และรู้สึกสงสัยว่าการทำงานหนักเกินไปและความเชี่ยวชาญพิเศษสามารถสร้างพฤติกรรมที่แตกต่างกันได้อย่างไร อย่างไรก็ตามฉันไม่ได้แนะนำให้ใช้งานมากเกินไป แต่เป็นความเชี่ยวชาญ เมื่อฉันใส่template <>ตัวอย่างแรกของคุณฉันได้ผลลัพธ์exp::swap(A, A)จาก gcc ทำไมไม่ชอบความเชี่ยวชาญพิเศษ?
voltrevo

1
ว้าว! นี่คือการรู้แจ้งจริงๆ คุณทำให้ฉันเชื่อมั่นอย่างแน่นอน ฉันคิดว่าฉันจะแก้ไขข้อเสนอแนะของคุณเล็กน้อยและใช้ไวยากรณ์ของเพื่อนในชั้นเรียนจาก Dave Abrahams (เฮ้ฉันสามารถใช้สิ่งนี้สำหรับตัวดำเนินการ << ได้เช่นกัน! :-)) เว้นแต่คุณจะมีเหตุผลที่จะหลีกเลี่ยงเช่นกัน (นอกเหนือจากการรวบรวม แยกต่างหาก) นอกจากนี้คุณคิดว่าusing std::swapเป็นข้อยกเว้นของกฎ "never put using statement in header files" หรือไม่? ที่จริงทำไมไม่ใส่using std::swapข้างใน<algorithm>? ฉันคิดว่ามันสามารถทำลายรหัสของคนส่วนน้อยได้ อาจจะเลิกสนับสนุนและนำไปใช้ในที่สุด?
voltrevo

3
ไวยากรณ์ของเพื่อนในชั้นเรียนน่าจะใช้ได้ ฉันจะพยายาม จำกัดusing std::swapขอบเขตฟังก์ชันภายในส่วนหัวของคุณ ใช่swapเกือบจะเป็นคำหลัก แต่ไม่มันไม่ใช่คำหลัก ดังนั้นอย่าส่งออกไปยังเนมสเปซทั้งหมดจนกว่าคุณจะต้องทำจริงๆ swapเป็นเหมือนoperator==มาก ความแตกต่างที่ใหญ่ที่สุดคือไม่เคยคิดที่จะเรียกoperator==ด้วยไวยากรณ์ของเนมสเปซที่มีคุณสมบัติเหมาะสม (มันจะน่าเกลียดเกินไป)
Howard Hinnant

15
@NielKirk: สิ่งที่คุณเห็นว่าเป็นภาวะแทรกซ้อนเป็นเพียงคำตอบที่ผิดมากเกินไป ไม่มีอะไรซับซ้อนเกี่ยวกับคำตอบที่ถูกต้องของ Dave Abrahams: "วิธีที่ถูกต้องในการสลับโอเวอร์โหลดคือการเขียนในเนมสเปซเดียวกับสิ่งที่คุณกำลังแลกเปลี่ยนเพื่อให้สามารถพบได้ผ่านการค้นหาตามอาร์กิวเมนต์ (ADL)"
Howard Hinnant

2
@codeshot: ขออภัย Herb พยายามดึงข้อความนี้มาตั้งแต่ปี 1998: gotw.ca/publications/mill02.htm เขาไม่ได้กล่าวถึงการแลกเปลี่ยนในบทความนี้ แต่นี่เป็นเพียงแอปพลิเคชันอื่นของ Herb's Interface Principle
Howard Hinnant

53

คุณไม่ได้รับอนุญาต (ตามมาตรฐาน C ++) ให้โอเวอร์โหลด std :: swap อย่างไรก็ตามคุณได้รับอนุญาตให้เพิ่มความเชี่ยวชาญเทมเพลตสำหรับประเภทของคุณเองลงในเนมสเปซมาตรฐาน เช่น

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

จากนั้นการใช้งานในคอนเทนเนอร์มาตรฐาน (และที่อื่น ๆ ) จะเลือกความเชี่ยวชาญของคุณแทนที่จะเป็นแบบทั่วไป

โปรดทราบว่าการจัดเตรียมการใช้งาน swap ระดับพื้นฐานไม่ดีพอสำหรับประเภทที่ได้รับของคุณ เช่นถ้าคุณมี

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

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

สิ่งนี้จะใช้ได้กับคลาสฐาน แต่ถ้าคุณพยายามสลับสองวัตถุที่ได้รับมามันจะใช้เวอร์ชันทั่วไปจาก std เนื่องจากการแลกเปลี่ยนเทมเพลตนั้นตรงกันทุกประการ (และหลีกเลี่ยงปัญหาในการสลับเฉพาะส่วน 'ฐาน' ของวัตถุที่ได้รับของคุณ )

หมายเหตุ: ฉันได้อัปเดตสิ่งนี้เพื่อลบบิตที่ผิดออกจากคำตอบสุดท้ายของฉัน D'โอ้! (ขอบคุณ puetzk และ j_random_hacker ที่ชี้ให้เห็น)


1
คำแนะนำที่ดีเป็นส่วนใหญ่ แต่ฉันต้อง -1 เนื่องจากความแตกต่างที่ลึกซึ้งที่ puetzk สังเกตเห็นระหว่างความเชี่ยวชาญเทมเพลตในเนมสเปซมาตรฐาน (ซึ่งได้รับอนุญาตโดยมาตรฐาน C ++) และการโอเวอร์โหลด (ซึ่งไม่ได้เป็น)
j_random_hacker

11
ลงคะแนนเนื่องจากวิธีที่ถูกต้องในการปรับแต่งการแลกเปลี่ยนคือการทำในเนมสเปซของคุณเอง (ดังที่ Dave Abrahams ชี้ให้เห็นในคำตอบอื่น)
Howard Hinnant

2
เหตุผลของฉันในการลดคะแนนก็เหมือนกับของ Howard
Dave Abrahams

13
@HowardHinnant, Dave Abrahams: ฉันไม่เห็นด้วย คุณอ้างว่าทางเลือกของคุณ "ถูกต้อง" บนพื้นฐานใด ดังที่ puetzk ยกมาจากมาตรฐานจึงอนุญาตโดยเฉพาะ ในขณะที่ฉันยังใหม่กับปัญหานี้ฉันไม่ชอบวิธีการที่คุณสนับสนุนเพราะถ้าฉันกำหนด Foo และเปลี่ยนวิธีที่คนอื่นที่ใช้รหัสของฉันมีแนวโน้มที่จะใช้ std :: swap (a, b) มากกว่า swap ( a, b) บน Foo ซึ่งใช้เวอร์ชันเริ่มต้นที่ไม่มีประสิทธิภาพอย่างเงียบ ๆ
voltrevo

5
@ Mozza314: ข้อ จำกัด ของพื้นที่และการจัดรูปแบบของพื้นที่แสดงความคิดเห็นไม่อนุญาตให้ฉันตอบกลับคุณอย่างเต็มที่ โปรดดูคำตอบที่ฉันได้เพิ่มหัวข้อ "Attention Mozza314"
Howard Hinnant

29

แม้ว่าจะถูกต้อง แต่โดยทั่วไปไม่ควรเพิ่มสิ่งต่างๆลงใน std :: namespace แต่อนุญาตให้เพิ่มความเชี่ยวชาญเทมเพลตสำหรับประเภทที่ผู้ใช้กำหนดโดยเฉพาะ ไม่ได้ใช้ฟังก์ชันมากเกินไป นี่คือความแตกต่างเล็กน้อย :-)

17.4.3.1/1 ไม่ได้กำหนดไว้สำหรับโปรแกรม C ++ ในการเพิ่มการประกาศหรือคำจำกัดความให้กับ namespace std หรือ namespaces ที่มี namespace std เว้นแต่จะระบุไว้เป็นอย่างอื่น โปรแกรมอาจเพิ่มความเชี่ยวชาญเทมเพลตสำหรับเทมเพลตไลบรารีมาตรฐานใด ๆ ลงใน namespace std ความเชี่ยวชาญพิเศษดังกล่าว (ทั้งหมดหรือบางส่วน) ของไลบรารีมาตรฐานส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดเว้นแต่การประกาศจะขึ้นอยู่กับชื่อที่ผู้ใช้กำหนดเองของการเชื่อมโยงภายนอกและเว้นแต่ความเชี่ยวชาญของเทมเพลตจะตรงตามข้อกำหนดไลบรารีมาตรฐานสำหรับเทมเพลตดั้งเดิม

ความเชี่ยวชาญของ std :: swap จะมีลักษณะดังนี้:

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

หากไม่มีเทมเพลต <> บิตมันจะเกินพิกัดซึ่งไม่ได้กำหนดไว้แทนที่จะเป็นความเชี่ยวชาญพิเศษซึ่งได้รับอนุญาต วิธีการแนะนำของ @ Wilka ในการเปลี่ยนเนมสเปซเริ่มต้นอาจใช้งานได้กับรหัสผู้ใช้ (เนื่องจากการค้นหา Koenig เลือกใช้เวอร์ชันที่ไม่มีเนมสเปซน้อยกว่า) แต่ไม่รับประกันและในความเป็นจริงไม่ควรใช้ (การใช้งาน STL ควรใช้อย่างเต็มที่ -qualified std :: swap)

มีเธรดบน comp.lang.c ++. กลั่นกรองโดยใช้การตัดสลับยาวของหัวข้อ ส่วนใหญ่เป็นเรื่องเกี่ยวกับความเชี่ยวชาญเฉพาะบางส่วน (ซึ่งปัจจุบันยังไม่มีวิธีที่ดีในการทำ)


7
เหตุผลหนึ่งที่ผิดในการใช้ความเชี่ยวชาญเทมเพลตฟังก์ชันสำหรับสิ่งนี้ (หรืออะไรก็ได้): มันโต้ตอบในรูปแบบที่ไม่ดีกับการโอเวอร์โหลดซึ่งมีจำนวนมากสำหรับการแลกเปลี่ยน ตัวอย่างเช่นหากคุณเชี่ยวชาญ std :: swap ปกติสำหรับ std :: vector <mytype> & ความเชี่ยวชาญของคุณจะไม่ได้รับเลือกจาก swap เฉพาะเวกเตอร์ของมาตรฐานเนื่องจากความเชี่ยวชาญพิเศษไม่ได้รับการพิจารณาในระหว่างการแก้ปัญหาโอเวอร์โหลด
Dave Abrahams

4
นี่คือสิ่งที่เมเยอร์สแนะนำใน C ++ 3ed ที่มีประสิทธิภาพ (Item 25, pp 106-112)
jww

2
@DaveAbrahams: ถ้าคุณมีความเชี่ยวชาญ (โดยไม่ขัดแย้งแม่แบบชัดเจน) การสั่งซื้อบางส่วนจะทำให้มันเป็นความเชี่ยวชาญของvectorรุ่นและมันจะถูกนำมาใช้
Davis Herring

1
@DavisHerring จริงไม่เมื่อคุณทำการสั่งซื้อบางส่วนจะไม่มีบทบาท ปัญหาไม่ได้อยู่ที่คุณไม่สามารถเรียกมันได้เลย มันคือสิ่งที่เกิดขึ้นเมื่อมีการสลับโอเวอร์โหลดที่เฉพาะเจาะจงน้อยลงอย่างเห็นได้ชัด: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams

2
@DaveAbrahams: การสั่งซื้อบางส่วนคือการเลือกเทมเพลตฟังก์ชันที่จะเชี่ยวชาญเมื่อความเชี่ยวชาญเฉพาะทางตรงกับมากกว่าหนึ่งรายการ การ::swapโอเวอร์โหลดที่คุณเพิ่มนั้นมีความเชี่ยวชาญมากกว่าการstd::swapโอเวอร์โหลดvectorดังนั้นจึงจับการโทรได้และไม่มีความเชี่ยวชาญใด ๆ ที่เกี่ยวข้อง ฉันไม่แน่ใจว่านั่นเป็นปัญหาในทางปฏิบัติอย่างไร (แต่ฉันก็ไม่ได้อ้างว่านี่เป็นความคิดที่ดี!)
Davis Herring
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.