เหตุใดจึงต้องใช้ฟังก์ชันเริ่มต้นและสิ้นสุดที่ไม่ใช่สมาชิกใน C ++ 11


197

คอนเทนเนอร์มาตรฐานทุกตัวมี a beginและendเมธอดสำหรับส่งคืนตัววนซ้ำสำหรับคอนเทนเนอร์นั้น อย่างไรก็ตาม C ++ 11 ได้เปิดตัวฟังก์ชั่นฟรีที่เรียกว่าstd::beginและฟังก์ชั่นการstd::endโทรbeginและendสมาชิก ดังนั้นแทนที่จะเขียน

auto i = v.begin();
auto e = v.end();

คุณต้องการเขียน

auto i = std::begin(v);
auto e = std::end(v);

ในคำพูดของเขาการเขียน Modern C ++ Herb Sutter บอกว่าคุณควรใช้ฟังก์ชั่นฟรีทันทีเมื่อคุณต้องการเริ่มต้นหรือสิ้นสุดตัววนซ้ำสำหรับคอนเทนเนอร์ อย่างไรก็ตามเขาไม่ได้ลงรายละเอียดว่าทำไมคุณถึงต้องการ การดูรหัสจะช่วยให้คุณประหยัดอักขระทั้งหมดหนึ่งตัว ดังนั้นเท่าที่ภาชนะมาตรฐานไปฟังก์ชั่นฟรีดูเหมือนจะไร้ประโยชน์อย่างสมบูรณ์ Herb Sutter ระบุว่ามีประโยชน์สำหรับภาชนะที่ไม่ได้มาตรฐาน แต่อีกครั้งเขาไม่ได้ลงรายละเอียด

ดังนั้นคำถามคือสิ่งที่รุ่นฟังก์ชั่นอิสระของstd::beginและstd::endทำนอกเหนือจากการเรียกรุ่นฟังก์ชั่นสมาชิกที่สอดคล้องกันและทำไมคุณต้องการที่จะใช้พวกเขา?


29
มันเป็นตัวละครที่น้อยลงหนึ่งตัวบันทึกจุดเหล่านั้นสำหรับลูก ๆ ของคุณ: xkcd.com/297
HostileFork พูดว่าอย่าเชื่อถือ SE

ฉันเกลียดที่จะใช้มันเพราะฉันต้องทำซ้ำstd::ตลอดเวลา
Michael Chourdakis

คำตอบ:


162

คุณจะโทร.begin()และ.end()ใช้ C-array ได้อย่างไร?

ฟังก์ชั่นฟรีอนุญาตให้เขียนโปรแกรมทั่วไปได้มากขึ้นเพราะสามารถเพิ่มได้หลังจากนั้นในโครงสร้างข้อมูลที่คุณไม่สามารถเปลี่ยนแปลงได้


7
@JonathanMDavis: คุณสามารถมีendอาร์เรย์ที่ประกาศแบบคงที่ ( int foo[5]) โดยใช้เทคนิคการเขียนโปรแกรมแม่แบบ เมื่อมันสลายตัวไปยังตัวชี้แล้วคุณก็โชคดี
Matthieu M.

33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh

6
@JonathanMDavis: ตามที่คนอื่น ๆ ระบุไว้แน่นอนว่าเป็นไปได้ที่จะได้รับbeginและendอยู่ในอาร์เรย์ C ตราบใดที่คุณยังไม่ได้สลายมันไปยังตัวชี้ - @Huw สะกดมันออกมา สำหรับเหตุผลที่คุณต้องการ: ลองจินตนาการว่าคุณได้รับการปรับโครงสร้างโค้ดที่ใช้อาร์เรย์เพื่อใช้เวกเตอร์ (หรือในทางกลับกันไม่ว่าด้วยเหตุผลใดก็ตาม) หากคุณได้รับการใช้beginและendและบางทีบาง typedeffing ฉลาดรหัสการดำเนินการจะไม่ได้มีการเปลี่ยนแปลงที่ทุกคน (ยกเว้นบางทีบางส่วนของ typedefs) ที่
Karl Knechtel

31
@JonathanMDavis: อาร์เรย์ไม่ใช่ตัวชี้ และสำหรับทุกคน: เพื่อยุติความสับสนที่โด่งดังนี้ให้หยุดอ้างถึงพอยน์เตอร์ (บางส่วน) ว่าเป็น "อาร์เรย์ที่สลายตัว" ไม่มีคำศัพท์ดังกล่าวในภาษาและไม่มีประโยชน์สำหรับมัน พอยน์เตอร์เป็นพอยน์เตอร์, อาร์เรย์คืออาร์เรย์ อาร์เรย์สามารถแปลงเป็นตัวชี้ไปยังองค์ประกอบแรกของพวกเขาโดยปริยาย แต่ก็ยังคงเป็นเพียงตัวชี้เก่าปกติโดยไม่มีความแตกต่างกับผู้อื่น แน่นอนว่าคุณไม่สามารถรับ "สิ้นสุด" ของพอยน์เตอร์ได้ตัวพิมพ์เล็กและใหญ่ปิด
GManNickG

5
นอกจากอาร์เรย์แล้วยังมี API จำนวนมากที่เปิดเผยคอนเทนเนอร์เช่นลักษณะ เห็นได้ชัดว่าคุณไม่สามารถแก้ไข API ของบุคคลที่สามได้ แต่คุณสามารถเขียนฟังก์ชั่นเริ่มต้น / สิ้นสุดได้อย่างง่ายดาย
edA-qa mort-ora-y

35

พิจารณากรณีและปัญหาเมื่อคุณมีห้องสมุดที่มีคลาส:

class SpecialArray;

มันมี 2 วิธี:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

ในการวนซ้ำค่าที่คุณต้องสืบทอดจากคลาสนี้และกำหนดbegin()และend()วิธีการสำหรับกรณีเมื่อ

auto i = v.begin();
auto e = v.end();

แต่ถ้าคุณใช้อยู่เสมอ

auto i = begin(v);
auto e = end(v);

คุณสามารถทำสิ่งนี้:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

ซึ่งSpecialArrayIteratorเป็นสิ่งที่ต้องการ:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

ตอนนี้iและeสามารถใช้อย่างถูกกฎหมายสำหรับการวนซ้ำและการเข้าถึงค่าของ SpecialArray


8
สิ่งนี้ไม่ควรรวมถึงtemplate<>เส้น คุณกำลังประกาศฟังก์ชั่นโอเวอร์โหลดใหม่ไม่ใช่เทมเพลตที่เชี่ยวชาญ
David Stone

33

การใช้beginและendฟังก์ชั่นฟรีเพิ่มชั้นหนึ่งของการอ้อม โดยปกติจะทำเพื่อให้มีความยืดหยุ่นมากขึ้น

ในกรณีนี้ฉันนึกถึงการใช้น้อย

การใช้งานที่ชัดเจนที่สุดคือสำหรับ C-arrays (ไม่ใช่ตัวชี้ c)

อีกวิธีหนึ่งคือเมื่อพยายามใช้อัลกอริธึมมาตรฐานบนคอนเทนเนอร์ที่ไม่สอดคล้อง (เช่นคอนเทนเนอร์ไม่มี.begin()วิธี) สมมติว่าคุณไม่สามารถแก้ไขคอนเทนเนอร์ได้ตัวเลือกที่ดีที่สุดถัดไปคือโอเวอร์โหลดbeginฟังก์ชัน สมุนไพรแนะนำให้คุณใช้beginฟังก์ชั่นเพื่อส่งเสริมความสม่ำเสมอและความสม่ำเสมอในรหัสของคุณ แทนที่จะต้องจำว่าคอนเทนเนอร์ใดรองรับวิธีbeginและต้องการฟังก์ชันbeginและฟังก์ชั่นที่จำเป็น

เช่นกันถัด c ++ รอบควรคัดลอก D's สัญกรณ์หลอกสมาชิก หากไม่ได้กำหนดมันแทนพยายามa.foo(b,c,d) foo(a,b,c,d)มันเป็นเพียงน้ำตาลประโยคเล็ก ๆ น้อย ๆ ที่จะช่วยให้มนุษย์ที่ยากจนซึ่งชอบวิชาและการเรียงคำกริยา


5
สัญกรณ์หลอกสมาชิกลักษณะเช่น C # /. สุทธิวิธีการขยาย สิ่งเหล่านี้มีประโยชน์สำหรับสถานการณ์ที่หลากหลาย - เช่นเดียวกับฟีเจอร์ทั้งหมด - มีแนวโน้มที่จะ 'ละเมิด'
Gareth Wilson

5
สัญกรณ์หลอกสมาชิกเป็นประโยชน์สำหรับการเข้ารหัสด้วย Intellisense; กดปุ่ม "a." แสดงคำกริยาที่เกี่ยวข้อง, ปลดปล่อยพลังสมองจากการจดจำรายการและช่วยค้นหาฟังก์ชั่น API ที่เกี่ยวข้องสามารถช่วยป้องกันการทำงานซ้ำซ้อนโดยไม่จำเป็นต้องใช้ฟังก์ชั่นที่ไม่ใช่สมาชิกของ shoehorn ในชั้นเรียน
Matt Curtis

มีข้อเสนอเพื่อรับสิ่งนั้นใน C ++ ซึ่งใช้คำว่า Unified Function Call Syntax (UFCS)
underscore_d

17

เพื่อตอบคำถามของคุณฟังก์ชั่นฟรีเริ่มต้น () และ end () โดยค่าเริ่มต้นทำอะไรได้มากกว่าการเรียกฟังก์ชั่นสมาชิก. begin () และ. end () ของคอนเทนเนอร์ จากการ<iterator>รวมโดยอัตโนมัติเมื่อคุณใช้ใด ๆ ของภาชนะมาตรฐานเช่น<vector>, <list>ฯลฯ คุณจะได้รับ:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

ส่วนที่สองของคำถามของคุณคือทำไมชอบฟังก์ชั่นฟรีถ้าพวกเขาทำคือเรียกฟังก์ชั่นสมาชิกต่อไป ขึ้นอยู่กับว่าวัตถุชนิดใดvในโค้ดตัวอย่างของคุณ หากประเภทของ v เป็นประเภทคอนเทนเนอร์มาตรฐานvector<T> v;ก็ไม่เป็นไรถ้าคุณใช้ฟังก์ชั่นฟรีหรือสมาชิกพวกเขาทำสิ่งเดียวกัน หากวัตถุของคุณvเป็นแบบทั่วไปเช่นในรหัสต่อไปนี้:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

จากนั้นใช้ฟังก์ชั่นสมาชิกแบ่งรหัสของคุณสำหรับอาร์เรย์ T = C, สตริง C, enums ฯลฯ โดยใช้ฟังก์ชั่นที่ไม่ใช่สมาชิกคุณโฆษณาอินเทอร์เฟซทั่วไปเพิ่มเติมที่ผู้คนสามารถขยายได้อย่างง่ายดาย โดยใช้อินเทอร์เฟซฟังก์ชันฟรี:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

ตอนนี้โค้ดทำงานร่วมกับอาร์เรย์ T = C และสตริง C ตอนนี้เขียนรหัสของอะแดปเตอร์จำนวนเล็กน้อย:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

เราสามารถทำให้โค้ดของคุณใช้งานร่วมกับ enums ได้ ฉันคิดว่าประเด็นหลักของ Herb ก็คือการใช้ฟังก์ชั่นฟรีนั้นง่ายเหมือนการใช้ฟังก์ชั่นสมาชิกและมันให้โค้ดของคุณย้อนกลับเข้ากันได้กับประเภทลำดับ C และความเข้ากันได้ไปข้างหน้ากับชนิดลำดับ non-stl ด้วยต้นทุนที่ต่ำกับผู้พัฒนารายอื่น


ตัวอย่างที่ดี ฉันจะไม่ใช้enumประเภทพื้นฐานหรืออื่น ๆ โดยอ้างอิง แต่; พวกเขาจะถูกกว่าการทำสำเนามากกว่าทางอ้อม
underscore_d

6

ข้อดีอย่างหนึ่งของstd::beginและstd::endคือพวกเขาทำหน้าที่เป็นจุดส่วนขยายสำหรับการใช้อินเตอร์เฟซมาตรฐานสำหรับชั้นเรียนภายนอก

หากคุณต้องการใช้CustomContainerคลาสที่มีช่วงแบบอิงฟังก์ชันลูปหรือเทมเพลตที่คาดหวัง.begin()และ.end()วิธีการคุณต้องใช้วิธีการเหล่านั้นอย่างชัดเจน

หากชั้นเรียนมีวิธีการเหล่านั้นแสดงว่าไม่มีปัญหา หากไม่เป็นเช่นนั้นคุณจะต้องแก้ไข *

สิ่งนี้ไม่เป็นไปได้เสมอเช่นเมื่อใช้ห้องสมุดภายนอกแหล่งข้อมูลเชิงพาณิชย์และแหล่งปิด

ในสถานการณ์เช่นนี้std::beginและstd::endมีประโยชน์เนื่องจากเราสามารถให้ iterator API โดยไม่ต้องแก้ไขคลาสเอง แต่ให้โหลดฟรีมากเกินไป

ตัวอย่าง:สมมติว่าคุณต้องการใช้count_ifฟังก์ชั่นที่ใช้คอนเทนเนอร์แทนตัววนซ้ำคู่หนึ่ง รหัสดังกล่าวอาจมีลักษณะเช่นนี้:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

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

ตอนนี้ C ++ มีกลไกที่เรียกว่าArgument Dependent Lookup (ADL) ซึ่งทำให้วิธีการดังกล่าวมีความยืดหยุ่นมากยิ่งขึ้น

กล่าวโดยย่อคือ ADL หมายถึงว่าเมื่อคอมไพเลอร์แก้ไขฟังก์ชันที่ไม่มีเงื่อนไข (เช่นฟังก์ชันที่ไม่มีเนมสเปซเหมือนbeginแทนstd::begin) จะพิจารณาฟังก์ชันที่ประกาศในเนมสเปซของอาร์กิวเมนต์ ตัวอย่างเช่น:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

ในกรณีนี้มันไม่สำคัญว่าชื่อที่ผ่านการรับรองจะเป็นsome_lib::beginและsome_lib::end - เนื่องจากCustomContainerมีอยู่some_lib::ด้วยคอมไพเลอร์จะใช้โอเวอร์โหลดเหล่านั้นcount_ifผู้ที่อยู่ใน

นั่นเป็นเหตุผลสำหรับการมีusing std::begin;และในusing std::end; count_ifสิ่งนี้ช่วยให้เราสามารถใช้งานได้อย่างไม่มีเงื่อนไขbeginและendอนุญาตให้ ADL และ อนุญาตให้คอมไพเลอร์เลือกstd::beginและstd::endเมื่อไม่พบทางเลือกอื่น

เราสามารถกินคุกกี้และมีคุกกี้ - เช่นมีวิธีที่จะให้การใช้งานที่กำหนดเองของbegin/ endในขณะที่คอมไพเลอร์สามารถถอยกลับไปที่มาตรฐาน

หมายเหตุบางส่วน:

  • ด้วยเหตุผลเดียวกันนี้ยังมีฟังก์ชั่นอื่น ๆ ที่คล้ายกัน: std::rbegin/ rend, และstd::sizestd::data

  • ในฐานะที่เป็นคำตอบอื่น ๆ กล่าวถึงstd::รุ่นที่มีมากเกินไปสำหรับอาร์เรย์เปล่า มีประโยชน์ แต่เป็นเพียงกรณีพิเศษของสิ่งที่ฉันได้อธิบายไว้ข้างต้น

  • การใช้std::beginและเพื่อน ๆ เป็นความคิดที่ดีโดยเฉพาะเมื่อเขียนโค้ดเทมเพลตเพราะสิ่งนี้ทำให้เทมเพลตเหล่านั้นมีความเป็นสากลมากขึ้น สำหรับแม่แบบที่ไม่ใช่คุณอาจใช้วิธีการเช่นกันเมื่อมี

ป.ล. ฉันทราบว่าโพสต์นี้เกือบ 7 ปีแล้ว ฉันเจอเพราะฉันต้องการตอบคำถามที่ถูกทำเครื่องหมายว่าซ้ำซ้อนและค้นพบว่าไม่มีคำตอบที่นี่ที่ระบุถึง ADL


คำตอบที่ดีโดยเฉพาะการอธิบาย ADL อย่างเปิดเผยแทนที่จะปล่อยให้มันเป็นไปตามจินตนาการเหมือนที่คนอื่นทำ - แม้ว่าพวกเขาจะแสดงมันออกมา!
underscore_d

5

ในขณะที่ฟังก์ชั่นที่ไม่ได้เป็นสมาชิกนั้นไม่ได้ให้ประโยชน์ใด ๆ สำหรับคอนเทนเนอร์มาตรฐานการใช้มันจะบังคับสไตล์ที่สอดคล้องและยืดหยุ่นมากขึ้น ถ้าในบางครั้งคุณต้องการที่จะขยายคลาส container ที่ไม่ใช่ std อยู่คุณควรกำหนด overloads ของฟังก์ชั่นฟรีแทนการเปลี่ยนนิยามของคลาสที่มีอยู่ ดังนั้นสำหรับคอนเทนเนอร์ที่ไม่ใช่ std พวกมันมีประโยชน์มากและมักจะใช้ฟังก์ชั่นฟรีทำให้โค้ดของคุณมีความยืดหยุ่นมากขึ้นในการที่คุณสามารถแทนที่คอนเทนเนอร์ std ด้วยคอนเทนเนอร์ที่ไม่ใช่ std ได้ง่ายขึ้นและประเภทคอนเทนเนอร์ต้นแบบนั้นโปร่งใสมากกว่าโค้ดของคุณ รองรับการใช้งานคอนเทนเนอร์ที่หลากหลายมากขึ้น

แต่แน่นอนว่าสิ่งนี้จะต้องมีการถ่วงน้ำหนักอย่างถูกต้องและสิ่งที่เป็นนามธรรมก็ไม่ดีเช่นกัน แม้ว่าการใช้ฟังก์ชั่นฟรีนั้นไม่ได้เป็นสิ่งที่เกินความเป็นจริง แต่ก็ยังสามารถใช้งานร่วมกับรหัส C ++ 03 ได้ซึ่งตอนนี้อายุน้อยกว่า C ++ 11 อาจยังคงเป็นปัญหาสำหรับคุณ


3
ใน C ++ 03, คุณก็สามารถใช้boost::begin()/ end()ดังนั้นไม่มีการเข้ากันไม่ได้จริง :)
มาร์ค Mutz - mmutz

1
@ MarcMutz-mmutz เอาละการเพิ่มความน่าเชื่อถือไม่ใช่ตัวเลือกเสมอไป (และค่อนข้าง overkill ถ้าใช้สำหรับเท่านั้นbegin/end) ดังนั้นฉันจะถือว่าความเข้ากันไม่ได้กับ C ++ บริสุทธิ์เช่นกัน แต่อย่างที่บอกไปมันเป็นความไม่เข้ากัน (และเล็กลง) ที่ค่อนข้างเล็กเนื่องจาก C ++ 11 (อย่างน้อยbegin/endโดยเฉพาะ) จะได้รับการยอมรับมากขึ้นเรื่อย ๆ
Christian Rau

0

ในที่สุดประโยชน์อยู่ในรหัสที่เป็นลักษณะทั่วไปเช่นนั้นเป็นผู้ไม่เชื่อเรื่องพระเจ้าคอนเทนเนอร์ มันสามารถทำงานกับ a std::vector, อาร์เรย์หรือช่วงโดยไม่ต้องเปลี่ยนรหัสตัวเอง

นอกจากนี้คอนเทนเนอร์แม้คอนเทนเนอร์ที่ไม่ได้เป็นเจ้าของสามารถทำการติดตั้งเพิ่มเติมเพื่อให้สามารถใช้ซ้ำโดยใช้รหัสโดยใช้ accessors ที่ไม่ได้เป็นสมาชิกช่วง

ดูที่นี่สำหรับรายละเอียดเพิ่มเติม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.