สำนวนนี้คืออะไรและควรใช้เมื่อใด ปัญหาอะไรที่แก้ได้? สำนวนเปลี่ยนไปเมื่อใช้ C ++ 11 หรือไม่?
แม้ว่ามันจะถูกกล่าวถึงในหลาย ๆ ที่ แต่เราก็ไม่มีคำถามและคำตอบว่า "มันคืออะไร" มันจึงเป็นเช่นนี้ นี่คือรายการบางส่วนของสถานที่ที่เคยกล่าวถึง:
สำนวนนี้คืออะไรและควรใช้เมื่อใด ปัญหาอะไรที่แก้ได้? สำนวนเปลี่ยนไปเมื่อใช้ C ++ 11 หรือไม่?
แม้ว่ามันจะถูกกล่าวถึงในหลาย ๆ ที่ แต่เราก็ไม่มีคำถามและคำตอบว่า "มันคืออะไร" มันจึงเป็นเช่นนี้ นี่คือรายการบางส่วนของสถานที่ที่เคยกล่าวถึง:
คำตอบ:
ระดับที่จัดการทรัพยากร (กเสื้อคลุมเหมือนตัวชี้สมาร์ท) ความต้องการที่จะใช้บิ๊กสาม ในขณะที่เป้าหมายและการนำไปใช้งานของตัวคัดลอกตัวสร้างและ destructor นั้นตรงไปตรงมาตัวดำเนินการกำหนดค่าคัดลอกเป็นเนื้อหาที่เหมาะสมและยากที่สุด ควรทำอย่างไร? ต้องหลีกเลี่ยงข้อผิดพลาดอะไรบ้าง
สำนวนการคัดลอกและการแลกเปลี่ยนคือการแก้ปัญหาและสวยงามช่วยให้ผู้ประกอบการที่ได้รับมอบหมายในการบรรลุสิ่งที่สอง: การหลีกเลี่ยงความซ้ำซ้อนรหัสและการให้การรับประกันข้อยกเว้นที่แข็งแกร่ง
แนวคิดก็ทำงานโดยใช้ฟังก์ชั่นการคัดลอกตัวสร้างเพื่อสร้างสำเนาของข้อมูลแล้วจะนำข้อมูลที่คัดลอกด้วยswap
ฟังก์ชั่นการแลกเปลี่ยนข้อมูลเก่าที่มีข้อมูลใหม่ สำเนาชั่วคราวนั้นจะทำลายโดยนำข้อมูลเก่าไปด้วย เราถูกทิ้งไว้พร้อมกับสำเนาของข้อมูลใหม่
เพื่อที่จะใช้สำนวนการคัดลอกและการแลกเปลี่ยนที่เราต้องสิ่งที่สาม: ทำงานคัดลอกตัวสร้างเป็น destructor ทำงาน (ทั้งเป็นพื้นฐานของการห่อหุ้มใด ๆ ดังนั้นควรจะอยู่แล้วสมบูรณ์) และswap
ฟังก์ชั่น
ฟังก์ชั่นสลับเป็นฟังก์ชั่นที่ไม่ใช่การขว้างปาที่แลกเปลี่ยนสองวัตถุของคลาสสมาชิกสำหรับสมาชิก เราอาจถูกล่อลวงให้ใช้std::swap
แทนการจัดหาของเราเอง แต่สิ่งนี้จะเป็นไปไม่ได้ std::swap
ใช้ตัวคัดลอกคอนสตรัคเตอร์และตัวดำเนินการคัดลอกการมอบหมายภายในการนำไปใช้และท้ายที่สุดเราจะพยายามกำหนดโอเปอเรเตอร์การมอบหมายในแง่ของตัวเอง!
(ไม่เพียงแค่นั้น แต่การโทรอย่างไม่มีเงื่อนไขswap
จะใช้ตัวดำเนินการแลกเปลี่ยนที่กำหนดเองของเราข้ามการก่อสร้างที่ไม่จำเป็นและการทำลายชั้นเรียนของเราที่std::swap
จะนำมาซึ่ง)
ลองพิจารณากรณีที่เป็นรูปธรรม เราต้องการจัดการในอาเรย์แบบไดนามิกที่ไร้ประโยชน์ เราเริ่มต้นด้วย constructor ที่ใช้งานได้ copy-constructor และ destructor:
#include <algorithm> // std::copy
#include <cstddef> // std::size_t
class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}
// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr),
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}
// destructor
~dumb_array()
{
delete [] mArray;
}
private:
std::size_t mSize;
int* mArray;
};
คลาสนี้จัดการอาร์เรย์ได้สำเร็จ แต่ต้องoperator=
ทำงานอย่างถูกต้อง
นี่คือวิธีการใช้งานที่ไร้เดียงสาอาจมีลักษณะ:
// the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)
// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}
return *this;
}
และเราบอกว่าเราทำเสร็จแล้ว ตอนนี้จัดการอาร์เรย์โดยไม่มีการรั่วไหล (n)
แต่ก็ทนทุกข์ทรมานจากปัญหาสามเครื่องหมายตามลำดับในรหัสเป็น
ที่แรกก็คือการทดสอบการมอบหมายด้วยตนเอง การตรวจสอบนี้มีจุดประสงค์สองประการ: เป็นวิธีที่ง่ายในการป้องกันไม่ให้เราเรียกใช้โค้ดที่ไม่มีความจำเป็นในการกำหนดตนเองและจะป้องกันเราจากข้อบกพร่องเล็กน้อย (เช่นการลบอาร์เรย์เพื่อลองและคัดลอกเท่านั้น) แต่ในกรณีอื่น ๆ มันทำหน้าที่เพียงแค่ทำให้โปรแกรมช้าลงและทำหน้าที่เป็นเสียงรบกวนในโค้ด การกำหนดตนเองไม่ค่อยเกิดขึ้นดังนั้นส่วนใหญ่เวลาตรวจสอบนี้เป็นของเสีย มันจะดีกว่าถ้าผู้ปฏิบัติงานสามารถทำงานได้อย่างถูกต้องหากไม่มีมัน
ประการที่สองคือให้การรับประกันข้อยกเว้นพื้นฐานเท่านั้น หากnew int[mSize]
ล้มเหลว*this
จะได้รับการแก้ไข (กล่าวคือขนาดไม่ถูกต้องและข้อมูลหายไป!) สำหรับการรับประกันข้อยกเว้นที่แข็งแกร่งจะต้องเป็นสิ่งที่คล้ายกับ:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get the new data ready before we replace the old
std::size_t newSize = other.mSize;
int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
// replace the old data (all are non-throwing)
delete [] mArray;
mSize = newSize;
mArray = newArray;
}
return *this;
}
รหัสได้ขยาย! ซึ่งนำเราไปสู่ปัญหาที่สาม: การทำสำเนารหัส ผู้ดำเนินการที่ได้รับมอบหมายของเราทำซ้ำรหัสทั้งหมดที่เราเคยเขียนไว้ที่อื่นอย่างมีประสิทธิภาพและนั่นเป็นสิ่งที่แย่มาก
ในกรณีของเราแกนกลางของมันเป็นเพียงสองบรรทัด (การจัดสรรและการคัดลอก) แต่ด้วยทรัพยากรที่ซับซ้อนมากขึ้นรหัสนี้ขยายตัวได้ค่อนข้างยุ่งยาก เราควรพยายามไม่ทำซ้ำตัวเอง
(หนึ่งอาจสงสัย: ถ้ารหัสนี้มากเป็นสิ่งจำเป็นในการจัดการทรัพยากรอย่างถูกต้องสิ่งที่ถ้าชั้นเรียนของฉันจัดการมากกว่าหนึ่งขณะนี้อาจดูเหมือนจะเป็นกังวลที่ถูกต้องและแน่นอนมันต้องไม่น่ารำคาญtry
/ catch
ข้อนี้เป็นที่ไม่ใช่ - ใหม่นั่นเป็นเพราะคลาสควรจัดการหนึ่งทรัพยากรเท่านั้น !)
ดังที่กล่าวไว้สำนวนการคัดลอกและสลับจะแก้ไขปัญหาเหล่านี้ทั้งหมด แต่ตอนนี้เรามีข้อกำหนดทั้งหมดยกเว้นหนึ่งswap
ฟังก์ชัน: a ในขณะที่กฎของสามประสบความสำเร็จสร้างความดำรงอยู่ของเราคัดลอกตัวสร้างผู้ประกอบการที่ได้รับมอบหมายและ destructor ก็ควรจริงๆจะเรียกว่า "บิ๊กสามและครึ่ง" เวลาใด class ของคุณจัดการทรัพยากรก็ยังทำให้ความรู้สึกที่จะให้เป็นswap
ฟังก์ชั่น .
เราจำเป็นต้องเพิ่มฟังก์ชั่นการแลกเปลี่ยนในชั้นเรียนของเราและเราทำสิ่งดังต่อไปนี้†:
class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;
// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
};
( นี่คือคำอธิบายว่าทำไมpublic friend swap
) ตอนนี้ไม่เพียง แต่เราสามารถแลกเปลี่ยนของเราdumb_array
ได้ แต่การแลกเปลี่ยนโดยทั่วไปจะมีประสิทธิภาพมากกว่า มันแค่แลกเปลี่ยนพอยน์เตอร์และขนาดแทนที่จะจัดสรรและคัดลอกอาร์เรย์ทั้งหมด นอกเหนือจากโบนัสนี้ในการทำงานและประสิทธิภาพตอนนี้เราพร้อมที่จะใช้สำนวนการคัดลอกและสลับ
หากไม่มีความกังวลใจเพิ่มเติมผู้ดำเนินการที่ได้รับมอบหมายของเราคือ:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
และนั่นมัน! ด้วยการล้มลงหนึ่งปัญหาทั้งสามได้รับการจัดการอย่างสง่างามในครั้งเดียว
ครั้งแรกที่เราสังเกตเห็นทางเลือกที่สำคัญ: อาร์กิวเมนต์พารามิเตอร์จะนำมาโดยมีมูลค่า ในขณะที่ใคร ๆ ก็สามารถทำสิ่งต่อไปนี้ได้อย่างง่ายดาย
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
เราสูญเสียโอกาสในการเพิ่มประสิทธิภาพที่สำคัญ ไม่เพียงแค่นั้น แต่ตัวเลือกนี้มีความสำคัญใน C ++ 11 ซึ่งจะกล่าวถึงในภายหลัง (ในบันทึกทั่วไปคำแนะนำที่มีประโยชน์อย่างน่าสังเกตมีดังต่อไปนี้: หากคุณกำลังจะทำสำเนาของบางอย่างในฟังก์ชันให้คอมไพเลอร์ทำในรายการพารามิเตอร์‡)
ไม่ว่าจะด้วยวิธีใดวิธีการรับทรัพยากรของเรานี้เป็นกุญแจสำคัญในการกำจัดการทำสำเนารหัส: เราจะใช้รหัสจากตัวสร้างสำเนาเพื่อสร้างสำเนาและไม่จำเป็นต้องทำซ้ำบิตใด ๆ ตอนนี้ทำสำเนาเราก็พร้อมที่จะแลกเปลี่ยน
สังเกตว่าเมื่อเข้าสู่ฟังก์ชั่นที่ข้อมูลใหม่ทั้งหมดได้รับการจัดสรรคัดลอกและพร้อมใช้งานแล้ว นี่คือสิ่งที่ทำให้เรามีการรับประกันข้อยกเว้นที่แข็งแกร่งสำหรับฟรี: *this
เราจะไม่ได้ใส่ฟังก์ชั่นหากการก่อสร้างของการคัดลอกล้มเหลวและมันจึงเป็นไปไม่ได้ที่จะปรับเปลี่ยนสถานะของ (สิ่งที่เราทำด้วยตนเองมาก่อนเพื่อการรับประกันข้อยกเว้นที่แข็งแกร่งคอมไพเลอร์กำลังทำเพื่อเราในตอนนี้ชนิดไหน)
ณ จุดนี้เราปลอดบ้านเพราะswap
ไม่มีการขว้างปา เราสลับข้อมูลปัจจุบันของเรากับข้อมูลที่คัดลอกเปลี่ยนแปลงสถานะของเราอย่างปลอดภัยและข้อมูลเก่าจะถูกนำไปใช้เป็นข้อมูลชั่วคราว ข้อมูลเก่าจะถูกปล่อยออกมาเมื่อฟังก์ชั่นกลับมา (โดยที่ขอบเขตของพารามิเตอร์สิ้นสุดลงและตัวทำลายของมันถูกเรียก)
เนื่องจากสำนวนนั้นไม่มีรหัสซ้ำเราจึงไม่สามารถแนะนำบั๊กภายในโอเปอเรเตอร์ได้ โปรดทราบว่านี่หมายความว่าเราไม่จำเป็นต้องมีการตรวจสอบการมอบหมายด้วยตนเองoperator=
อีกต่อไป (นอกจากนี้เราจะไม่ได้รับโทษปรับประสิทธิภาพจากการไม่ได้มอบหมายตนเอง)
และนั่นคือสำนวนคัดลอกและสลับ
รุ่นถัดไปของ C ++, C ++ 11 ทำให้การเปลี่ยนแปลงสำคัญอย่างหนึ่งสำหรับวิธีการที่เราจัดการทรัพยากร: กฎของสามคือตอนนี้กฎของสี่ (และครึ่ง) ทำไม? เพราะไม่เพียง แต่เราจะต้องมีความสามารถในการคัดลอกสร้างทรัพยากรของเราที่เราจำเป็นต้องย้ายที่สร้างมันให้ดีที่สุด
โชคดีสำหรับเรานี่เป็นเรื่องง่าย:
class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
};
เกิดอะไรขึ้นที่นี่? ระลึกถึงเป้าหมายของการสร้างการเคลื่อนย้าย: เพื่อใช้ทรัพยากรจากอินสแตนซ์อื่นของคลาสปล่อยให้อยู่ในสภาพที่รับประกันว่าสามารถกำหนดและทำลายได้
ดังนั้นสิ่งที่เราทำนั้นง่ายมาก: เริ่มต้นผ่านตัวสร้างปริยาย (คุณสมบัติ C ++ 11) จากนั้นสลับกับother
; เรารู้ว่าอินสแตนซ์ที่สร้างขึ้นเป็นค่าเริ่มต้นของคลาสของเราสามารถกำหนดและทำลายได้อย่างปลอดภัยดังนั้นเราจึงรู้ว่าother
จะสามารถทำสิ่งเดียวกันได้หลังจากเปลี่ยน
(โปรดทราบว่าคอมไพเลอร์บางตัวไม่สนับสนุนตัวสร้างคอนสตรัคเตอร์ในกรณีนี้เราต้องเริ่มต้นสร้างคลาสด้วยตนเองนี่เป็นงานที่โชคร้าย แต่โชคดีมาก)
นั่นคือการเปลี่ยนแปลงเพียงอย่างเดียวที่เราจำเป็นต้องทำในชั้นเรียนของเราดังนั้นทำไมมันถึงได้ผล? จดจำการตัดสินใจที่สำคัญที่สุดที่เราทำเพื่อทำให้พารามิเตอร์เป็นค่าและไม่ใช่การอ้างอิง:
dumb_array& operator=(dumb_array other); // (1)
ตอนนี้ถ้าother
จะถูกเริ่มต้นด้วยการ rvalue, มันจะย้ายที่สร้าง สมบูรณ์ ในทำนองเดียวกัน C ++ 03 ให้เราใช้ฟังก์ชั่นตัวสร้างสำเนาของเราอีกครั้งโดยรับอาร์กิวเมนต์โดยค่า C ++ 11 จะเลือกตัวสร้างการย้ายโดยอัตโนมัติเมื่อเหมาะสมเช่นกัน (และแน่นอนตามที่กล่าวถึงในบทความที่เชื่อมโยงก่อนหน้านี้การคัดลอก / การเคลื่อนย้ายของค่าอาจรวมเข้าด้วยกัน)
ดังนั้นสรุปสำนวนคัดลอกและแลกเปลี่ยน
* ทำไมเราmArray
ถึงตั้งค่าเป็นโมฆะ? เพราะถ้ามีรหัสเพิ่มเติมใด ๆ ในโอเปอเรเตอร์ที่ส่งไปdumb_array
จะมีการเรียกdestructor ของ และหากสิ่งนั้นเกิดขึ้นโดยไม่ตั้งค่าเป็นโมฆะเราพยายามลบหน่วยความจำที่ถูกลบไปแล้ว! เราหลีกเลี่ยงสิ่งนี้ด้วยการตั้งค่าเป็นโมฆะเนื่องจากการลบโมฆะเป็นการไม่ดำเนินการ
claims มีข้อเรียกร้องอื่น ๆ ที่เราควรมีความเชี่ยวชาญstd::swap
สำหรับประเภทของเราจัดให้มีswap
ฟังก์ชั่นฟรีในชั้นเรียนswap
และอื่น ๆ แต่สิ่งนี้ไม่จำเป็นทั้งหมด: การใช้ที่เหมาะสมใด ๆswap
จะเป็นการโทรที่ไม่มีเงื่อนไขและฟังก์ชั่นของเราจะ พบได้ผ่านADL ฟังก์ชั่นหนึ่งจะทำ
‡เหตุผลง่าย ๆ : เมื่อคุณมีทรัพยากรให้กับตัวเองแล้วคุณสามารถสลับและ / หรือย้าย (C ++ 11) ได้ทุกที่ที่ต้องการ และโดยการทำสำเนาในรายการพารามิเตอร์คุณเพิ่มประสิทธิภาพสูงสุด
construct คอนสตรัคเตอร์ย้ายควรเป็นnoexcept
มิฉะนั้นรหัสบางอย่าง (เช่นstd::vector
การปรับขนาดตรรกะ) จะใช้ตัวสร้างการคัดลอกแม้ว่าการย้ายจะทำให้เกิดความรู้สึก แน่นอนเพียงทำเครื่องหมายว่าไม่รับการยกเว้นหากโค้ดด้านในไม่แสดงข้อยกเว้น
swap
ให้คุณพบระหว่าง ADL ถ้าคุณต้องการให้มันทำงานในรหัสทั่วไปส่วนใหญ่ที่คุณจะเจอเช่นboost::swap
และอื่น ๆ เช่นการแลกเปลี่ยน Swap เป็นปัญหาที่ยากลำบากใน C ++ และโดยทั่วไปเราทุกคนต่างเห็นพ้องกันว่าจุดเข้าใช้งานที่ดีที่สุด (เพื่อความมั่นคง) และวิธีเดียวที่จะทำได้โดยทั่วไปคือฟังก์ชั่นฟรี ( int
ไม่สามารถมีสมาชิก swap ได้ ตัวอย่างเช่น). ดูคำถามของฉันสำหรับพื้นหลัง
การกำหนดที่เป็นหัวใจของมันคือสองขั้นตอน: การฉีกสถานะเก่าของวัตถุและสร้างสถานะใหม่เป็นสำเนาของสถานะของวัตถุอื่น ๆ
โดยพื้นฐานนั่นคือสิ่งที่ผู้ทำลายและผู้สร้างสำเนาทำดังนั้นความคิดแรกคือการมอบหมายงานให้พวกเขา อย่างไรก็ตามเนื่องจากการทำลายจะต้องไม่ล้มเหลวขณะที่การก่อสร้างอาจ, เราจริงต้องการที่จะทำวิธีอื่น ๆ : ครั้งแรกที่ดำเนินการในส่วนที่สร้างสรรค์และหากที่ประสบความสำเร็จแล้วทำส่วนการทำลายล้าง copy-and-swap idiom เป็นวิธีที่จะทำเช่นนั้น: มันเรียกตัวสร้างสำเนาของคลาสเพื่อสร้างวัตถุชั่วคราวจากนั้นทำการแลกเปลี่ยนข้อมูลกับวัตถุชั่วคราวแล้วปล่อยให้ผู้ทำลายชั่วคราวทำลายสถานะเก่า
ตั้งแต่swap()
ควรจะไม่ล้มเหลวส่วนเดียวที่อาจล้มเหลวคือการสร้างสำเนา สิ่งนี้ถูกดำเนินการก่อนและหากล้มเหลวจะไม่มีการเปลี่ยนแปลงอะไรในวัตถุเป้าหมาย
ในรูปแบบที่ละเอียดแล้วการคัดลอกและสลับจะดำเนินการโดยให้มีการคัดลอกดำเนินการโดยการเริ่มต้นพารามิเตอร์ (ไม่อ้างอิง) ของผู้ประกอบการที่ได้รับมอบหมาย:
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
std::swap(this_string, that)
ไม่รับประกันไม่มีการโยน มันให้ความปลอดภัยข้อยกเว้นที่แข็งแกร่ง แต่ไม่รับประกันไม่มีโยน
std::string::swap
(ซึ่งถูกเรียกโดยstd::swap
) ใน C ++ 0x std::string::swap
คือnoexcept
และต้องไม่ส่งข้อยกเว้น
std::array
... )
มีคำตอบที่ดีอยู่แล้ว ผมจะเน้นส่วนใหญ่เกี่ยวกับสิ่งที่ผมคิดว่าพวกเขาขาด - คำอธิบายของ "ข้อเสีย" กับสำนวนการคัดลอกและการแลกเปลี่ยนที่ ....
สำนวนคัดลอกและแลกเปลี่ยนคืออะไร?
วิธีใช้งานโอเปอเรเตอร์การมอบหมายในแง่ของฟังก์ชั่นสลับ
X& operator=(X rhs)
{
swap(rhs);
return *this;
}
แนวคิดพื้นฐานคือ:
ส่วนที่เกิดข้อผิดพลาดได้ง่ายที่สุดในการกำหนดให้กับวัตถุคือทำให้แน่ใจว่าทรัพยากรใด ๆ ที่ได้รับความต้องการของรัฐใหม่ (เช่นหน่วยความจำ, descriptors)
การได้มานั้นสามารถทำได้ก่อนที่จะแก้ไขสถานะปัจจุบันของวัตถุ (เช่น*this
) หากทำสำเนาของค่าใหม่ซึ่งเป็นสาเหตุที่rhs
ได้รับการยอมรับโดยค่า (เช่นคัดลอก) มากกว่าโดยอ้างอิง
สลับสถานะของสำเนาท้องถิ่นrhs
และ*this
เป็นมักจะค่อนข้างง่ายที่จะทำโดยไม่ต้องมีศักยภาพความล้มเหลว / ข้อยกเว้นให้สำเนาไม่จำเป็นต้องรัฐใด ๆ หลังจากนั้น (เพียงแค่ต้องการพอดีรัฐสำหรับ destructor ในการทำงานให้มากที่สุดเท่าสำหรับวัตถุที่ถูกย้ายจากใน> = C ++ 11)
ควรใช้เมื่อใด (ปัญหาใดบ้างที่แก้ไข[/ สร้าง] ?)
เมื่อคุณต้องการมอบหมายให้คัดค้านไม่ได้รับผลกระทบจากการมอบหมายที่ส่งข้อยกเว้นสมมติว่าคุณมีหรือสามารถเขียนswap
ด้วยการรับประกันข้อยกเว้นที่แข็งแกร่งและในอุดมคติที่ไม่สามารถล้มเหลว / throw
.. †
เมื่อคุณต้องการวิธีทำความสะอาดที่เข้าใจง่ายและมีประสิทธิภาพในการกำหนดตัวดำเนินการกำหนดค่าในรูปของswap
ฟังก์ชันตัวคัดลอก (ง่ายกว่า) และฟังก์ชันตัวทำลาย
† swap
ขว้างปา: มันเป็นไปได้ที่จะเชื่อถือข้อมูลสมาชิกแลกเปลี่ยนว่าวัตถุติดตามโดยตัวชี้ แต่ไม่ใช่ตัวชี้ข้อมูลสมาชิกที่ไม่ได้มีการแลกเปลี่ยนโยนฟรีหรือที่แลกเปลี่ยนจะต้องมีการดำเนินการเป็นX tmp = lhs; lhs = rhs; rhs = tmp;
และคัดลอกการก่อสร้างหรือการกำหนด อาจมีการโยน แต่ก็ยังมีโอกาสที่จะล้มเหลวในการทิ้งข้อมูลสมาชิกบางส่วนที่เปลี่ยนและอื่น ๆ ไม่ได้ ความเป็นไปได้นี้ใช้ได้แม้กับ C ++ 03std::string
ในขณะที่เจมส์แสดงความคิดเห็นกับคำตอบอื่น:
@wilhelmtell: ใน C ++ 03 ไม่มีการกล่าวถึงข้อยกเว้นที่อาจเกิดขึ้นจาก std :: string :: swap (ซึ่งถูกเรียกโดย std :: swap) ใน C ++ 0x, std :: string :: swap คือ noexcept และต้องไม่ส่งข้อยกเว้น - James McNellis 22 ธ.ค. 53 เวลา 15:24 น
implementation การดำเนินการของผู้ประกอบการที่ได้รับมอบหมายซึ่งดูเหมือนว่ามีเหตุผลเมื่อกำหนดจากวัตถุที่แตกต่างสามารถล้มเหลวในการกำหนดตนเองได้อย่างง่ายดาย แม้ว่ามันอาจดูเหมือนเป็นไปไม่ได้ที่รหัสไคลเอนต์จะพยายามกำหนดตนเอง แต่มันสามารถเกิดขึ้นได้ค่อนข้างง่ายในระหว่างการดำเนินงานอัลโกบนคอนเทนเนอร์ด้วยx = f(x);
รหัสที่f
(อาจมีเฉพาะบาง#ifdef
สาขา) แมโคร ala #define f(x) x
หรือฟังก์ชันที่ส่งคืนการอ้างอิงถึงx
หรือ (น่าจะไม่มีประสิทธิภาพ แต่รัดกุม) รหัสเหมือนx = c1 ? x * 2 : c2 ? x / 2 : x;
) ตัวอย่างเช่น:
struct X
{
T* p_;
size_t size_;
X& operator=(const X& rhs)
{
delete[] p_; // OUCH!
p_ = new T[size_ = rhs.size_];
std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
}
...
};
เมื่อวันที่ตัวเองได้รับมอบหมายของโค้ดด้านบนลบx.p_;
จุดp_
ในเขตกองจัดสรรใหม่แล้วพยายามที่จะอ่านuninitialisedข้อมูลนั้น (พฤติกรรมที่ไม่ได้กำหนด) หากที่ไม่ได้ทำอะไรแปลกเกินไป, copy
ความพยายามในการที่มีการกำหนดตัวเองทุก just- ถูกทำลาย 'T'!
copy สำนวนคัดลอกและแลกเปลี่ยนสามารถนำเสนอความไร้ประสิทธิภาพหรือข้อ จำกัด เนื่องจากการใช้งานชั่วคราวพิเศษ (เมื่อพารามิเตอร์ของผู้ประกอบการถูกสร้างขึ้นคัดลอก):
struct Client
{
IP_Address ip_address_;
int socket_;
X(const X& rhs)
: ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
{ }
};
ที่นี่การเขียนด้วยมือClient::operator=
อาจตรวจสอบว่า*this
เชื่อมต่อกับเซิร์ฟเวอร์เดียวกันกับrhs
(อาจส่งรหัส "รีเซ็ต" หากมีประโยชน์) ในขณะที่วิธีการคัดลอกและสลับจะเรียกใช้ตัวคัดลอกที่จะเขียนเพื่อเปิด การเชื่อมต่อซ็อกเก็ตที่แตกต่างจากนั้นปิดอันเดิม ไม่เพียงแค่นั้นอาจหมายถึงการมีปฏิสัมพันธ์เครือข่ายระยะไกลแทนการคัดลอกตัวแปรในกระบวนการอย่างง่าย แต่ก็สามารถเรียกใช้ไคลเอนต์หรือเซิร์ฟเวอร์ข้อ จำกัด ในทรัพยากรซ็อกเก็ตหรือการเชื่อมต่อ (แน่นอนว่าคลาสนี้มีอินเทอร์เฟซที่น่ากลัว แต่นั่นเป็นอีกเรื่อง ;-P)
Client
คือไม่ได้รับอนุญาต
คำตอบนี้เป็นเหมือนการเพิ่มและการแก้ไขคำตอบด้านบนเล็กน้อย
ในบางเวอร์ชันของ Visual Studio (และอาจเป็นคอมไพเลอร์อื่น ๆ ) มีข้อผิดพลาดที่น่ารำคาญจริงๆและไม่สมเหตุสมผล ดังนั้นหากคุณประกาศ / กำหนดswap
ฟังก์ชั่นของคุณเช่นนี้:
friend void swap(A& first, A& second) {
std::swap(first.size, second.size);
std::swap(first.arr, second.arr);
}
... คอมไพเลอร์จะตะโกนใส่คุณเมื่อคุณเรียกใช้swap
ฟังก์ชัน:
สิ่งนี้เกี่ยวข้องกับfriend
ฟังก์ชันที่เรียกใช้และthis
วัตถุถูกส่งผ่านเป็นพารามิเตอร์
วิธีนี้คือการไม่ใช้friend
คำหลักและกำหนดswap
ฟังก์ชัน:
void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
}
เวลานี้คุณสามารถโทรswap
และส่งต่อได้other
ทำให้คอมไพเลอร์มีความสุข:
ท้ายที่สุดคุณไม่จำเป็นต้องใช้friend
ฟังก์ชั่นในการสลับ 2 วัตถุ มันสมเหตุสมผลมากที่จะทำให้swap
ฟังก์ชั่นสมาชิกที่มีหนึ่งother
วัตถุเป็นพารามิเตอร์
คุณมีสิทธิ์เข้าถึงthis
วัตถุอยู่แล้วดังนั้นจึงผ่านเป็นพารามิเตอร์ซ้ำซ้อนทางเทคนิค
friend
การเรียกใช้ฟังก์ชันพร้อม*this
พารามิเตอร์
ฉันต้องการเพิ่มคำเตือนเมื่อคุณกำลังจัดการกับคอนเทนเนอร์ C ++ แบบ 11 ตัวรู้การจัดสรร การแลกเปลี่ยนและการมอบหมายมีความหมายที่แตกต่างกันอย่างละเอียด
เพื่อความเป็นรูปธรรมขอให้เราพิจารณาคอนเทนเนอร์std::vector<T, A>
ซึ่งA
เป็นประเภทตัวจัดสรรแบบไร้รัฐและเราจะเปรียบเทียบฟังก์ชันต่อไปนี้:
void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{
a.swap(b);
b.clear(); // not important what you do with b
}
void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
a = std::move(b);
}
วัตถุประสงค์ของทั้งสองฟังก์ชั่นfs
และfm
คือการให้a
รัฐที่b
มีในขั้นต้น อย่างไรก็ตามมีคำถามที่ซ่อนอยู่: เกิดอะไรขึ้นถ้าa.get_allocator() != b.get_allocator()
? คำตอบคือ: ขึ้นอยู่กับ มาเขียนAT = std::allocator_traits<A>
กัน
หากAT::propagate_on_container_move_assignment
เป็นstd::true_type
เช่นนั้นfm
ให้มอบหมายการจัดสรรใหม่a
ด้วยค่าb.get_allocator()
มิฉะนั้นจะไม่ใช้และa
ยังคงใช้ตัวจัดสรรดั้งเดิมต่อไป ในกรณีนั้นองค์ประกอบของข้อมูลจะต้องสลับเป็นรายบุคคลเนื่องจากการจัดเก็บa
และb
ไม่เข้ากัน
ถ้าAT::propagate_on_container_swap
เป็นstd::true_type
เช่นนั้นfs
แลกเปลี่ยนข้อมูลและตัวจัดสรรในแบบที่คาดไว้
ถ้าAT::propagate_on_container_swap
เป็นstd::false_type
เช่นนั้นเราต้องตรวจสอบแบบไดนามิก
a.get_allocator() == b.get_allocator()
จากนั้นทั้งสองคอนเทนเนอร์ใช้ที่เก็บข้อมูลที่เข้ากันได้และการแลกเปลี่ยนจะดำเนินต่อไปตามปกติa.get_allocator() != b.get_allocator()
โปรแกรมนั้นมีพฤติกรรมที่ไม่ได้กำหนด (cf. [container.requirements.general / 8]ผลที่สุดคือการแลกเปลี่ยนกลายเป็นการดำเนินการที่ไม่น่าสนใจใน C ++ 11 ทันทีที่คอนเทนเนอร์ของคุณเริ่มสนับสนุนการจัดสรรที่มีสถานะ นั่นเป็น "กรณีการใช้งานขั้นสูง" แต่ก็ไม่น่าเป็นไปได้ทั้งหมดเนื่องจากการเพิ่มประสิทธิภาพการย้ายมักจะน่าสนใจเมื่อชั้นเรียนของคุณจัดการทรัพยากรและหน่วยความจำเป็นหนึ่งในทรัพยากรที่ได้รับความนิยมมากที่สุด