ฉันจะรับที่อยู่ของวัตถุได้อย่างน่าเชื่อถือเมื่อโอเปอเรเตอร์ & มากเกินไปได้อย่างไร


170

พิจารณาโปรแกรมต่อไปนี้:

struct ghost
{
    // ghosts like to pretend that they don't exist
    ghost* operator&() const volatile { return 0; }
};

int main()
{
    ghost clyde;
    ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( 
}

ฉันจะได้รับclydeที่อยู่ของได้อย่างไร

ฉันกำลังมองหาทางออกที่จะทำงานได้ดีกับวัตถุทุกประเภท โซลูชัน C ++ 03 ก็ดี แต่ฉันก็สนใจ C ++ 11 เช่นกัน ถ้าเป็นไปได้ลองหลีกเลี่ยงพฤติกรรมการใช้งานเฉพาะ

ฉันทราบถึงstd::addressofเทมเพลตฟังก์ชันของ C ++ 11 แต่ไม่สนใจที่จะใช้ที่นี่: ฉันต้องการทำความเข้าใจว่าผู้ใช้ไลบรารีมาตรฐานอาจใช้เทมเพลตฟังก์ชันนี้ได้อย่างไร


41
@jalf: กลยุทธ์ที่เป็นที่ยอมรับ แต่ตอนนี้ฉันได้เจาะคนกล่าวในหัวฉันจะทำงานรอบ ๆ รหัสที่น่ารังเกียจของพวกเขาได้อย่างไร :-)
James McNellis

5
@jalf Uhm บางครั้งคุณต้องโอเวอร์โหลดโอเปอเรเตอร์นี้และส่งคืนพร็อกซีออบเจ็กต์ แม้ว่าตอนนี้ฉันจะไม่นึกถึงตัวอย่าง
Konrad Rudolph

5
@ Konrad: ฉันทั้งสอง หากคุณต้องการสิ่งนั้นฉันขอแนะนำว่าตัวเลือกที่ดีกว่าอาจเป็นการคิดใหม่การออกแบบของคุณเพราะการใช้งานมากเกินไปทำให้เกิดปัญหามากเกินไป :)
jalf

2
@ Konrad: ในการประมาณ 20 ปีของการเขียนโปรแกรม C ++ ฉันเคยพยายามที่จะโอเวอร์โหลดตัวดำเนินการนั้น นั่นเป็นจุดเริ่มต้นของยี่สิบปีที่ผ่านมา โอ้และฉันไม่สามารถใช้งานได้ ด้วยเหตุนี้รายการคำถามที่พบบ่อยของผู้ปฏิบัติงานการบรรทุกเกินพิกัดจะบอกว่า คุณจะได้รับเบียร์ฟรีในครั้งต่อไปที่เราพบว่าคุณสามารถหาตัวอย่างที่น่าเชื่อถือสำหรับการใช้งานเกินกำลัง (ฉันรู้ว่าคุณกำลังจะออกจากกรุงเบอร์ลินเพื่อให้ฉันได้อย่างปลอดภัยสามารถนำเสนอนี้:))
เอสบีไอ

5
CComPtr<>และCComQIPtr<>มีงานมากไปoperator&
ไซมอนริกเตอร์

คำตอบ:


102

ปรับปรุง:ใน C ++ 11 หนึ่งอาจจะใช้แทนstd::addressofboost::addressof


ให้เราคัดลอกรหัสจาก Boost ก่อนลบคอมไพเลอร์ทำงานกับบิต:

template<class T>
struct addr_impl_ref
{
  T & v_;

  inline addr_impl_ref( T & v ): v_( v ) {}
  inline operator T& () const { return v_; }

private:
  addr_impl_ref & operator=(const addr_impl_ref &);
};

template<class T>
struct addressof_impl
{
  static inline T * f( T & v, long ) {
    return reinterpret_cast<T*>(
        &const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
  }

  static inline T * f( T * v, int ) { return v; }
};

template<class T>
T * addressof( T & v ) {
  return addressof_impl<T>::f( addr_impl_ref<T>( v ), 0 );
}

จะเกิดอะไรขึ้นถ้าเราผ่านการอ้างอิงถึงฟังก์ชั่น ?

หมายเหตุ: addressofไม่สามารถใช้กับตัวชี้ไปยังฟังก์ชั่น

ใน C ++ หากvoid func();มีการประกาศไว้funcจะมีการอ้างอิงไปยังฟังก์ชันที่ไม่มีการโต้แย้งและไม่มีผลลัพธ์ การอ้างอิงถึงฟังก์ชั่นนี้สามารถแปลงเป็นตัวชี้ไปยังฟังก์ชั่นได้เล็กน้อยจาก - @Konstantinตาม: 13.3.3.2 ทั้งคู่T &และT *แยกไม่ออกสำหรับฟังก์ชัน ฟังก์ชั่นที่หนึ่งคือการแปลงข้อมูลประจำตัวและที่สองคือการแปลงฟังก์ชันเป็นตัวชี้ทั้งสองมีอันดับ "การจับคู่แบบตรงทั้งหมด" (13.3.3.1.1 ตารางที่ 9)

การอ้างอิงฟังก์ชันผ่านaddr_impl_refมีความคลุมเครือในการแก้ปัญหาโอเวอร์โหลดสำหรับตัวเลือกfซึ่งแก้ไขได้ด้วยอาร์กิวเมนต์ดัมมี่0ซึ่งเป็นintครั้งแรกและสามารถเลื่อนเป็นlong(การแปลงอินทิกรัล)

ดังนั้นเราเพียงแค่ส่งกลับตัวชี้

จะเกิดอะไรขึ้นถ้าเราส่งประเภทด้วยตัวดำเนินการแปลง

หากผู้ประกอบการแปลงให้ผลตอบแทนT*แล้วเรามีความกำกวม: สำหรับf(T&,long)การส่งเสริมการขายเป็นสิ่งจำเป็นสำหรับการโต้แย้งที่สองในขณะที่สำหรับf(T*,int)ผู้ประกอบการแปลงจะถูกเรียกในครั้งแรก(ขอบคุณ @litb)

นั่นคือเมื่อaddr_impl_refเริ่มใช้งานมาตรฐาน C ++ กำหนดว่าลำดับการแปลงอาจมีการแปลงที่ผู้ใช้กำหนดสูงสุดหนึ่งรายการ ด้วยการห่อประเภทaddr_impl_refและบังคับให้ใช้ลำดับการแปลงแล้วเราจะ "ปิด" ผู้ให้บริการการแปลงที่มาพร้อมกับประเภทนั้น

ดังนั้นจึงf(T&,long)มีการเลือกเกินพิกัด (และดำเนินการโปรโมตอินทิกรัล)

จะเกิดอะไรขึ้นกับคนอื่น ๆ ?

ดังนั้นการf(T&,long)เลือกเกินพิกัดเนื่องจากมีประเภทไม่ตรงกับT*พารามิเตอร์

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

เกิดอะไรขึ้นกับการโอเวอร์โหลดนี้

เราต้องการหลีกเลี่ยงการใช้operator&กับประเภทเนื่องจากอาจมีการใช้งานมากเกินไป

การรับรองมาตรฐานที่reinterpret_castอาจใช้กับงานนี้ (ดูคำตอบของ @Matteo Italia: 5.2.10 / 10)

Boost เพิ่มสิ่งที่น่าสนใจด้วยconstและตัวระบุvolatileเพื่อหลีกเลี่ยงคำเตือนของคอมไพเลอร์ (และใช้ a const_castเพื่อลบออกอย่างเหมาะสม)

  • ส่งT&ไปที่char const volatile&
  • เปลื้องผ้าconstและvolatile
  • ใช้&โอเปอเรเตอร์เพื่อจดที่อยู่
  • โยนกลับไปที่ T*

กระบวนการconst/ volatileการเล่นกลเป็นบิตของมนต์ดำ แต่มันทำให้งานง่ายขึ้น (แทนที่จะให้การโอเวอร์โหลดเกิน 4 ครั้ง) หมายเหตุว่าตั้งแต่Tเป็นอย่างไม่มีเงื่อนไขถ้าเราผ่านghost const&แล้วT*เป็นghost const*จึงบ่นยังไม่ได้รับมันหายไป

แก้ไข:ตัวชี้โอเวอร์โหลดใช้สำหรับตัวชี้ไปยังฟังก์ชั่นฉันแก้ไขคำอธิบายข้างต้นบ้าง ผมก็ยังไม่เข้าใจว่าทำไมมันเป็นสิ่งที่จำเป็นแม้ว่า

เอาท์พุท ideoneต่อไปนี้สรุปสิ่งนี้ขึ้นมาบ้าง


2
"จะเกิดอะไรขึ้นถ้าเราส่งตัวชี้ไป?" ส่วนไม่ถูกต้อง หากเราส่งตัวชี้ไปยังบางประเภท U ฟังก์ชัน address ของประเภท 'T' ถูกอนุมานว่า 'U *' และ addr_impl_ref จะมีสองโอเวอร์โหลด: 'f (U * &, long)' และ 'f (U **, int) 'เห็นได้ชัดว่าจะเลือกคนแรก
Konstantin Oznobihin

@ Konstantin: ใช่ฉันคิดว่าทั้งสองfโอเวอร์โหลดที่แม่แบบฟังก์ชั่นในขณะที่พวกเขาเป็นสมาชิกฟังก์ชั่นปกติของชั้นเรียนแม่แบบขอบคุณที่ชี้ให้เห็น (ตอนนี้ฉันแค่ต้องคิดออกว่าการใช้เกินพิกัดเคล็ดลับใด ๆ คืออะไร)
Matthieu M.

นี่เป็นคำตอบที่ดีมาก ฉันคิดว่ามันมีอะไรมากกว่านี้เพียงแค่ "ผ่านchar*ไป" ขอบคุณ Matthieu
James McNellis

@ James: ฉันได้รับความช่วยเหลืออย่างมากจาก @Konstantin ที่จะฟาดฟันด้วยไม้เมื่อใดก็ตามที่ฉันทำผิด: D
Matthieu M.

3
เหตุใดจึงต้องแก้ไขประเภทที่มีฟังก์ชันการแปลง มันจะไม่ชอบการแข่งขันที่แน่นอนกว่าการเรียกฟังก์ชั่นแปลงใด ๆ เพื่อT*? แก้ไข: ตอนนี้ฉันเห็น มันจะ แต่ด้วยการ0โต้แย้งมันจะจบลงด้วยการกากบาดดังนั้นจะคลุมเครือ
Johannes Schaub - litb

99

std::addressofใช้

คุณสามารถคิดได้ว่าเป็นการทำสิ่งต่อไปนี้เบื้องหลัง:

  1. ตีความวัตถุอีกครั้งเป็นการอ้างอิงถึงตัวอักษร
  2. ใช้ที่อยู่ของสิ่งนั้น (จะไม่เรียกโอเวอร์โหลด)
  3. เหวี่ยงตัวชี้กลับไปที่ตัวชี้ประเภทของคุณ

การใช้งานที่มีอยู่ (รวมถึง Boost.Addressof) ทำอย่างนั้นเพียงแค่ดูแลconstและvolatileคุณสมบัติเพิ่มเติม


16
ฉันชอบคำอธิบายนี้ดีกว่าตัวเลือกที่สามารถเข้าใจได้ง่าย
เลื่อน

49

เคล็ดลับที่อยู่เบื้องหลังboost::addressofและการดำเนินการโดย @Luc Danton ขึ้นอยู่กับความมหัศจรรย์ของreinterpret_cast; มาตรฐานระบุไว้ที่§5.2.10¶10อย่างชัดเจน

การแสดงออก lvalue ประเภทT1สามารถโยนไปประเภท“การอ้างอิงถึงT2” ถ้าการแสดงออกของประเภท“ชี้T1” สามารถแปลงอย่างชัดเจนชนิด“ชี้T2reinterpret_castใช้ นั่นคือการอ้างอิงการอ้างอิงreinterpret_cast<T&>(x)มีผลเช่นเดียวกับการแปลง*reinterpret_cast<T*>(&x)ด้วยบิวด์อิน&และ*โอเปอเรเตอร์ ผลลัพธ์คือ lvalue ที่อ้างถึงวัตถุเดียวกันกับ lvalue ต้นฉบับ แต่มีชนิดอื่น

ตอนนี้ช่วยให้เราสามารถแปลงการอ้างอิงวัตถุพลไปchar &(ที่มีวุฒิการศึกษาพันธุ์ถ้าอ้างอิงเป็นพันธุ์ที่มีคุณสมบัติ) เพราะตัวชี้ใด ๆ ที่สามารถแปลงเป็น char *(อาจจะเป็นพันธุ์ที่ผ่านการคัดเลือก) ตอนนี้เรามีchar &, ผู้ประกอบการที่บรรทุกเกินพิกัดบนวัตถุนั้นไม่มีความเกี่ยวข้องอีกต่อไปและเราสามารถรับที่อยู่กับ&ผู้ประกอบการในตัวได้

การใช้งานบูสต์เพิ่มสองสามขั้นตอนในการทำงานกับออบเจ็กต์ที่ผ่านการรับรอง cv: ขั้นตอนแรกreinterpret_castถูกดำเนินการเพื่อconst volatile char &มิฉะนั้นการchar &ส่งแบบธรรมดาจะไม่ทำงานconstและ / หรือvolatileการอ้างอิง ( reinterpret_castไม่สามารถลบได้const) จากนั้นconstและvolatileจะถูกลบด้วยconst_castที่อยู่จะถูกนำไปใช้&และจะเป็นประเภทสุดท้ายreinterpet_castที่ "ถูกต้อง"

const_castเป็นสิ่งจำเป็นในการลบconst/ volatileที่จะได้รับการเพิ่มไม่ใช่ const / อ้างอิงผันผวน แต่มันก็ไม่ได้ "อันตราย" สิ่งที่เป็นconst/ volatileการอ้างอิงในสถานที่แรกเพราะสุดท้ายreinterpret_castจะเป็นการเพิ่ม CV-คุณสมบัติถ้ามันเป็น มีในสถานที่แรก ( reinterpret_castไม่สามารถลบconstแต่สามารถเพิ่มได้)

ส่วนที่เหลือของรหัสในaddressof.hppดูเหมือนว่าส่วนใหญ่สำหรับการแก้ไขปัญหา static inline T * f( T * v, int )ดูเหมือนว่าจะมีความจำเป็นเฉพาะสำหรับคอมไพเลอร์บอร์แลนด์ แต่การปรากฏตัวของมันแนะนำความจำเป็นในการaddr_impl_refมิฉะนั้นประเภทตัวชี้จะถูกจับโดยเกินสองนี้

แก้ไขที่: overloads ต่างๆมีฟังก์ชั่นที่แตกต่างกันเห็น @Matthieu เอ็มคำตอบที่ดี

ฉันก็ไม่แน่ใจเหมือนกัน ฉันควรตรวจสอบรหัสนั้นเพิ่มเติม แต่ตอนนี้ฉันกำลังทำอาหารเย็น :) ฉันจะดูทีหลัง


คำอธิบายของ Matthieu M. เกี่ยวกับการส่งตัวชี้ไปยังที่อยู่นั้นไม่ถูกต้อง อย่าทำให้เสียคำตอบที่ยอดเยี่ยมของคุณด้วยการแก้ไข :)
Konstantin Oznobihin

"Appetit ดี" void func(); boost::addressof(func);แสดงให้เห็นว่าการตรวจสอบต่อไปว่าเกินพิกัดเรียกว่าสำหรับการอ้างอิงถึงฟังก์ชั่น อย่างไรก็ตามการลบโอเวอร์โหลดไม่ได้ป้องกัน gcc 4.3.4 จากการรวบรวมโค้ดและสร้างเอาต์พุตเดียวกันดังนั้นฉันยังไม่เข้าใจว่าทำไมจึงมีความจำเป็นที่จะต้องโอเวอร์โหลดนี้
Matthieu M.

@ Matthieu: มันดูเหมือนจะเป็นข้อผิดพลาดใน gcc ตาม 13.3.3.2 ทั้ง T & T * ไม่สามารถแยกฟังก์ชันได้ ฟังก์ชั่นที่หนึ่งคือการแปลงข้อมูลประจำตัวและที่สองคือการแปลงฟังก์ชันเป็นตัวชี้ทั้งสองมีอันดับ "การจับคู่แบบตรงทั้งหมด" (13.3.3.1.1 ตารางที่ 9) ดังนั้นจึงจำเป็นต้องมีการโต้แย้งเพิ่มเติม
Konstantin Oznobihin

@ Matthieu: เพิ่งลองกับ gcc 4.3.4 ( ideone.com/2f34P ) และมีความคลุมเครือตามที่คาดไว้ คุณลองใช้ฟังก์ชั่นสมาชิกมากเกินไปอย่างเช่นใน addressof Implementation หรือเท็มเพลตฟังก์ชั่นฟรีหรือไม่? อันหลัง (เช่นideone.com/vjCRs ) จะส่งผลให้มีการเลือกเกินพิกัด 'T *' เนื่องจากกฎการลดอาร์กิวเมนต์ temlate (14.8.2.1/2/2)
Konstantin Oznobihin

2
@curtguy: ทำไมคุณคิดว่ามันควร? ฉันได้อ้างถึงส่วนมาตรฐาน C ++ เฉพาะที่กำหนดสิ่งที่คอมไพเลอร์ควรทำและคอมไพเลอร์ทั้งหมดที่ฉันเข้าถึง (รวมถึง แต่ไม่ จำกัด เพียง gcc 4.3.4, comeau-online, VC6.0-VC2010) รายงานความคลุมเครือเช่นเดียวกับที่ฉันอธิบาย คุณช่วยอธิบายเหตุผลของคุณเกี่ยวกับคดีนี้ได้ไหม?
Konstantin Oznobihin

11

ฉันเคยเห็นการใช้งานของการaddressofทำสิ่งนี้:

char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);

อย่าถามฉันว่าการทำอย่างนี้เป็นอย่างไร!


5
ถูกกฎหมาย char*เป็นข้อยกเว้นที่ระบุไว้ในการพิมพ์กฎนามแฝง
ลูกสุนัข

6
@DeadMG ฉันไม่ได้บอกว่านี่ไม่สอดคล้อง ฉันบอกว่าคุณไม่ควรถามฉัน :)
ลัคดอง

1
@DeadMG ไม่มีปัญหาเรื่องนามแฝงที่นี่ คำถามคือ: มีการreinterpret_cast<char*>กำหนดไว้อย่างดี
curiousguy

2
@currguy และคำตอบคือใช่มันได้รับอนุญาตเสมอในการหล่อประเภทตัวชี้ใด ๆ[unsigned] char *และดังนั้นจึงอ่านการแสดงวัตถุของวัตถุแหลมที่ นี่เป็นอีกพื้นที่หนึ่งที่charมีสิทธิพิเศษ
underscore_d

@underscore_d เพียงเพราะนักแสดง "อนุญาตเสมอ" ไม่ได้หมายความว่าคุณสามารถทำอะไรกับผลลัพธ์ของนักแสดงได้
curiousguy

5

ลองดูที่boost :: addressofและการนำไปปฏิบัติ


1
รหัส Boost ในขณะที่น่าสนใจไม่ได้อธิบายว่าเทคนิคทำงานอย่างไร
James McNellis

คุณหมายถึง 'เกิน inline T * f (T * v, int)' เกิน? ดูเหมือนว่าจำเป็นสำหรับการแก้ปัญหา Borland C เท่านั้น วิธีการใช้มีตรงไปตรงมาสวย สิ่งที่บอบบาง (ไม่เป็นมาตรฐาน) เพียงอย่างเดียวคือการแปลง 'T &' เป็น 'char &' แม้ว่ามาตรฐานอนุญาตให้ส่งจาก 'T *' เป็น 'char *' ดูเหมือนว่าจะไม่มีข้อกำหนดดังกล่าวสำหรับการคัดเลือกอ้างอิง อย่างไรก็ตามหนึ่งอาจคาดหวังว่ามันจะทำงานเหมือนกันในคอมไพเลอร์ส่วนใหญ่
Konstantin Oznobihin

@Konstantin: overload ใช้เพราะตัวชี้addressofส่งคืนตัวชี้ สามารถพิสูจน์ได้ว่าเป็นสิ่งที่ผู้ใช้ต้องการหรือไม่ แต่เป็นวิธีที่ระบุไว้
Matthieu M.

@ Matthieu: คุณแน่ใจหรือไม่ เท่าที่ฉันสามารถบอกได้ชนิดใด ๆ (รวมถึงประเภทของตัวชี้) จะถูกห่อไว้ข้างในaddr_impl_refดังนั้นตัวชี้โอเวอร์โหลดจึงไม่ควรถูกเรียก ...
Matteo Italia

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