การจัดทำดัชนีเป็นโครงสร้างถูกกฎหมายหรือไม่?


104

ไม่ว่าโค้ดจะ 'แย่แค่ไหน' และสมมติว่าการจัดตำแหน่ง ฯลฯ ไม่ใช่ปัญหาในคอมไพเลอร์ / แพลตฟอร์มพฤติกรรมที่ไม่ได้กำหนดหรือเสียหรือไม่?

ถ้าฉันมีโครงสร้างแบบนี้: -

struct data
{
    int a, b, c;
};

struct data thing;

มันเป็นสิ่งที่ถูกต้องตามกฎหมายในการเข้าถึงa, bและcเป็น(&thing.a)[0], (&thing.a)[1]และ(&thing.a)[2]?

ในทุก ๆ คอมไพเลอร์และทุกแพลตฟอร์มที่ฉันลองใช้กับทุกการตั้งค่าที่ฉันลองมัน 'ได้ผล' ฉันแค่กังวลว่าคอมไพเลอร์อาจไม่ทราบว่าbและthing [1]เป็นสิ่งเดียวกันและเก็บค่า 'b' ไว้ในรีจิสเตอร์และสิ่งที่ [1] อ่านค่าผิดจากหน่วยความจำ (เช่น) ในทุกกรณีฉันพยายามทำในสิ่งที่ถูกต้อง (ฉันรู้แน่นอนว่ามันไม่ได้พิสูจน์อะไรมาก)

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

ติดแท็ก C และ C ++ ฉันสนใจ C ++ เป็นส่วนใหญ่ แต่ยังรวมถึง C หากแตกต่างกันเพียงเพื่อความสนใจ


51
ไม่มันไม่ใช่ "กฎหมาย" เป็นพฤติกรรมที่ไม่ได้กำหนด
Sam Varshavchik

10
มันเหมาะกับคุณในกรณีง่ายๆนี้เนื่องจากคอมไพเลอร์ไม่ได้เพิ่มช่องว่างระหว่างสมาชิก ลองใช้โครงสร้างโดยใช้ประเภทขนาดที่แตกต่างกันและจะพังลง
เพื่อนโปรแกรมเมอร์บางคน

7
ขุดอดีต - UB เคยถูกชื่อเล่นภูตจมูก
Adrian Colomitchi

21
เยี่ยมมากที่นี่ฉันสะดุดเพราะฉันติดตามแท็ก C อ่านคำถามจากนั้นเขียนคำตอบที่ใช้กับ C เท่านั้นเพราะฉันไม่เห็นแท็ก C ++ C และ C ++ แตกต่างกันมากที่นี่! C อนุญาตให้พิมพ์การตีด้วยสหภาพ C ++ ไม่ได้
Lundin

7
หากคุณต้องการเข้าถึงองค์ประกอบเป็นอาร์เรย์ให้กำหนดเป็นอาร์เรย์ หากจำเป็นต้องมีชื่ออื่นให้ใช้ชื่อ การพยายามมีเค้กของคุณและกินมันจะทำให้อาหารไม่ย่อยในที่สุด - อาจเป็นเวลาที่ไม่สะดวกเท่าที่จะจินตนาการได้ (ฉันคิดว่าดัชนี 0 ถูกต้องตามกฎหมายใน C ดัชนี 1 หรือ 2 ไม่ใช่มีบริบทที่องค์ประกอบเดียวถือว่าเป็นอาร์เรย์ขนาด 1)
Jonathan Leffler

คำตอบ:


73

ผิดกฎหมาย1 . นั่นเป็นพฤติกรรมที่ไม่ได้กำหนดใน C ++

คุณกำลังนำสมาชิกในรูปแบบอาร์เรย์ แต่นี่คือสิ่งที่มาตรฐาน C ++ กล่าวไว้ (เน้นของฉัน):

[dcl.array / 1] : ... วัตถุชนิดอาร์เรย์มีติดกันจัดสรรชุดไม่ว่างเปล่าของ subobjects N ประเภท T ...

แต่สำหรับสมาชิกไม่มีข้อกำหนดที่ต่อเนื่องกัน :

[class.mem / 17] : ... ; ข้อกำหนดการจัดตำแหน่งการนำไปใช้งานอาจทำให้สมาชิกสองคนที่อยู่ติดกันไม่ได้รับการจัดสรรทันทีหลังจากกัน ...

ในขณะที่คำพูดสองคำข้างต้นน่าจะเพียงพอที่จะบอกใบ้ว่าทำไมการจัดทำดัชนีเป็น a structตามที่คุณทำไม่ใช่พฤติกรรมที่กำหนดโดยมาตรฐาน C ++ ลองเลือกหนึ่งตัวอย่าง: ดูที่นิพจน์(&thing.a)[2]- เกี่ยวกับตัวดำเนินการตัวห้อย:

[expr.post//expr.sub/1] : นิพจน์ postfix ตามด้วยนิพจน์ในวงเล็บเหลี่ยมคือนิพจน์ postfix หนึ่งในนิพจน์ต้องเป็นค่า glvalue ของประเภท "array of T" หรือ prvalue ของประเภท "pointer to T" และอีกนิพจน์จะเป็น prvalue ของการแจงนับที่ไม่ได้กำหนดขอบเขตหรือประเภทปริพันธ์ ผลลัพธ์เป็นประเภท“ T” ประเภท“ T” ต้องเป็นประเภทอ็อบเจ็กต์ที่กำหนดโดยสมบูรณ์ 66 นิพจน์E1[E2]จะเหมือนกัน (ตามนิยาม) กับ((E1)+(E2))

เจาะลึกข้อความตัวหนาของคำพูดข้างต้น: เกี่ยวกับการเพิ่มประเภทอินทิกรัลให้กับประเภทตัวชี้ (สังเกตเน้นที่นี่) ..

[expr.add / 4] :เมื่อนิพจน์ที่มีชนิดอินทิกรัลถูกเพิ่มหรือลบออกจากตัวชี้ผลลัพธ์จะมีชนิดของตัวถูกดำเนินการตัวชี้ ถ้านิพจน์Pชี้ไปที่องค์ประกอบx[i]ของออบเจ็กต์อาร์เรย์ที่ มีองค์ประกอบ n นิพจน์และ(ที่มีค่า) จะชี้ไปที่องค์ประกอบ (อาจเป็นสมมุติฐาน) if; มิฉะนั้นจะไม่มีการกำหนดพฤติกรรม ...xP + JJ + PJjx[i + j]0 ≤ i + j ≤ n

หมายเหตุข้อกำหนดอาร์เรย์สำหรับif clause; อื่นมิฉะนั้นในใบเสนอราคาดังกล่าวข้างต้น (&thing.a)[2]เห็นได้ชัดว่านิพจน์ไม่เข้าเกณฑ์if clause; ดังนั้นพฤติกรรมที่ไม่ได้กำหนด


หมายเหตุด้านข้าง: แม้ว่าฉันได้ทดลองใช้โค้ดและรูปแบบต่างๆของคอมไพเลอร์ต่างๆอย่างกว้างขวางและพวกเขาไม่ได้แนะนำช่องว่างภายในใด ๆ ที่นี่ (ใช้งานได้ ); จากมุมมองการบำรุงรักษาโค้ดมีความเปราะบางมาก คุณควรยืนยันว่าการนำไปใช้งานได้จัดสรรสมาชิกอย่างต่อเนื่องก่อนดำเนินการนี้ และอยู่ในขอบเขต :-) แต่ยังไม่ได้กำหนดพฤติกรรม ....

คำตอบอื่น ๆ มีวิธีแก้ปัญหาที่เป็นไปได้



ตามที่ระบุไว้อย่างถูกต้องในความคิดเห็น[basic.lval / 8]ซึ่งอยู่ในการแก้ไขครั้งก่อนของฉันใช้ไม่ได้ ขอบคุณ @ 2501 และ @MM

1 : ดูคำตอบของ @ Barry สำหรับคำถามนี้สำหรับกรณีทางกฎหมายเดียวที่คุณสามารถเข้าถึงthing.aสมาชิกของโครงสร้างผ่านพาร์ทเทอร์นี้


1
@jcoder มันถูกกำหนดไว้ในclass.mem ดูย่อหน้าสุดท้ายสำหรับข้อความจริง
NathanOliver

4
การวางตัวที่เข้มงวดไม่เกี่ยวข้องที่นี่ ประเภท int มีอยู่ในประเภทการรวมและประเภทนี้อาจใช้นามแฝงว่า int - an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501

1
@ ผู้ลงคะแนนสนใจที่จะแสดงความคิดเห็น? - และเพื่อปรับปรุงหรือชี้ให้เห็นว่าคำตอบนี้ผิดตรงไหน?
WhiZTiM

4
การใช้นามแฝงที่เข้มงวดไม่เกี่ยวข้องกับสิ่งนี้ ช่องว่างภายในไม่ได้เป็นส่วนหนึ่งของค่าที่เก็บไว้ของวัตถุ นอกจากนี้คำตอบนี้ยังไม่สามารถระบุกรณีที่พบบ่อยที่สุด: จะเกิดอะไรขึ้นเมื่อไม่มีช่องว่างภายใน ขอแนะนำให้ลบคำตอบนี้จริง
MM

1
เสร็จแล้ว! ฉันได้ลบย่อหน้าเกี่ยวกับการใช้นามแฝงที่เข้มงวดแล้ว
WhiZTiM

48

ไม่ใน C นี่คือพฤติกรรมที่ไม่ได้กำหนดแม้ว่าจะไม่มีช่องว่างภายในก็ตาม

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

อย่างไรก็ตามคุณสามารถใช้ที่อยู่ของออบเจ็กต์โครงสร้างและคำนวณออฟเซ็ตเป็นสมาชิกเฉพาะ:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

สิ่งนี้ต้องทำสำหรับสมาชิกแต่ละคนทีละคน แต่สามารถใส่ลงในฟังก์ชันที่คล้ายกับการเข้าถึงอาร์เรย์ได้


1 (อ้างอิงจาก: ISO / IEC 9899: 201x 6.5.6 ตัวดำเนินการเพิ่มเติม 8)
ถ้าผลลัพธ์ชี้ให้เห็นองค์ประกอบสุดท้ายของออบเจ็กต์อาร์เรย์จะไม่ใช้เป็นตัวดำเนินการของตัวดำเนินการยูนารี * ที่ได้รับการประเมิน

2 (อ้างอิงจาก: ISO / IEC 9899: 201x 6.5.6 ตัวดำเนินการเพิ่มเติม 7)
สำหรับวัตถุประสงค์ของตัวดำเนินการเหล่านี้ตัวชี้ไปยังวัตถุที่ไม่ใช่องค์ประกอบของอาร์เรย์จะทำงานเหมือนกับตัวชี้ไปยังองค์ประกอบแรกของ อาร์เรย์ของความยาวหนึ่งที่มีประเภทของวัตถุเป็นประเภทองค์ประกอบ


3
โปรดทราบว่าสิ่งนี้ใช้ได้เฉพาะในกรณีที่ชั้นเรียนเป็นประเภทโครงร่างมาตรฐาน ถ้าไม่ใช่ก็ยังคงเป็น UB
NathanOliver

@NathanOliver ฉันควรพูดถึงว่าคำตอบของฉันใช้กับ C. แก้ไขเท่านั้น นี่เป็นปัญหาหนึ่งของคำถามภาษาแท็กคู่ดังกล่าว
2501

ขอบคุณและนั่นเป็นเหตุผลที่ฉันขอ C ++ และ C แยกกันเพราะมันน่าสนใจที่จะทราบความแตกต่าง
jcoder

@NathanOliver ที่อยู่ของสมาชิกคนแรกรับประกันว่าจะตรงกับที่อยู่ของคลาส C ++ หากเป็นเลย์เอาต์มาตรฐาน อย่างไรก็ตามไม่รับประกันว่าการเข้าถึงนั้นมีการกำหนดไว้อย่างชัดเจนและไม่ได้หมายความว่าการเข้าถึงดังกล่าวในคลาสอื่น ๆ ไม่ได้กำหนดไว้
Potatoswatter

คุณจะบอกว่านั่นchar* p = ( char* )&thing.a + offsetof( thing , b );นำไปสู่พฤติกรรมที่ไม่ได้กำหนด?
เอ็มเอ็ม

43

ใน C ++ หากคุณต้องการจริงๆ - สร้างตัวดำเนินการ []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

ไม่เพียงรับประกันว่าจะใช้งานได้ แต่การใช้งานนั้นง่ายกว่าคุณไม่จำเป็นต้องเขียนนิพจน์ที่อ่านไม่ได้ (&thing.a)[0]

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

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

โซลูชันนี้จะเปลี่ยนขนาดของโครงสร้างเพื่อให้คุณสามารถใช้วิธีการได้เช่นกัน:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

1
ฉันชอบที่จะเห็นการถอดชิ้นส่วนนี้เทียบกับการแยกชิ้นส่วนของโปรแกรม C โดยใช้การกดพิมพ์ แต่ แต่ ... C ++ เร็วเท่า C ... จริงไหม? ขวา?
Lundin

6
@Lundin หากคุณสนใจเกี่ยวกับความเร็วของการสร้างนี้ข้อมูลควรถูกจัดระเบียบเป็นอาร์เรย์ตั้งแต่แรกไม่ใช่เป็นฟิลด์แยกต่างหาก
Slava

2
@Lundin ทั้งในคุณหมายถึงพฤติกรรมที่อ่านไม่ได้และไม่ได้กำหนด? ไม่เป็นไรขอบคุณ.
Slava

1
@Lundin Operator overloading เป็นคุณลักษณะทางวากยสัมพันธ์ของเวลาคอมไพล์ที่ไม่ก่อให้เกิดค่าใช้จ่ายใด ๆ เมื่อเทียบกับฟังก์ชันปกติ ลองดูgodbolt.org/g/vqhREzเพื่อดูว่าคอมไพเลอร์ทำอะไรได้บ้างเมื่อคอมไพล์โค้ด C ++ และ C มันวิเศษมากในสิ่งที่พวกเขาทำและสิ่งที่พวกเขาคาดหวังให้พวกเขาทำ โดยส่วนตัวแล้วฉันชอบความปลอดภัยและการแสดงออกของ C ++ ที่ดีกว่า C ล้านเท่า และทำงานได้ตลอดเวลาโดยไม่ต้องอาศัยสมมติฐานเกี่ยวกับช่องว่างภายใน
Jens

2
การอ้างอิงเหล่านั้นจะเพิ่มขนาดของสิ่งนั้นเป็นสองเท่าเป็นอย่างน้อย thing.a()เพียงแค่ทำ
TC

14

สำหรับ c ++: หากคุณต้องการเข้าถึงสมาชิกโดยไม่ทราบชื่อคุณสามารถใช้ตัวแปรตัวชี้ไปยังสมาชิกได้

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

1
นี่คือการใช้สิ่งอำนวยความสะดวกทางภาษาและด้วยเหตุนี้จึงมีการกำหนดไว้อย่างดีและตามที่ฉันคิดว่ามีประสิทธิภาพ คำตอบที่ดีที่สุด
Peter - Reinstate Monica

2
สมมติว่ามีประสิทธิภาพ? ฉันถือว่าตรงกันข้าม ดูรหัสที่สร้างขึ้น
JDługosz

1
@ JDługoszคุณค่อนข้างถูกต้อง เมื่อมองไปที่ชุดประกอบที่สร้างขึ้นดูเหมือนว่า gcc 6.2 จะสร้างโค้ดที่เทียบเท่ากับการใช้offsetoffใน C.
StoryTeller - Unslander Monica

3
คุณยังสามารถปรับปรุงสิ่งต่างๆได้ด้วยการสร้าง arr constexpr สิ่งนี้จะสร้างตารางการค้นหาคงที่เดียวในส่วนข้อมูลแทนที่จะสร้างขึ้นมาทันที
ทิม

10

ใน ISO C99 / C11 การกดพิมพ์แบบยูเนี่ยนเป็นสิ่งที่ถูกกฎหมายดังนั้นคุณสามารถใช้แทนการจัดทำดัชนีพอยน์เตอร์ไปยังอาร์เรย์ที่ไม่ใช่ (ดูคำตอบอื่น ๆ )

ISO C ++ ไม่อนุญาตให้ใช้การพิมพ์แบบยูเนียน GNU C ++ เป็นส่วนขยายและฉันคิดว่าคอมไพเลอร์อื่น ๆ บางตัวที่ไม่รองรับส่วนขยาย GNU โดยทั่วไปรองรับการพิมพ์ยูเนี่ยน แต่นั่นไม่ได้ช่วยให้คุณเขียนโค้ดแบบพกพาได้อย่างเคร่งครัด

ด้วย gcc และ clang เวอร์ชันปัจจุบันการเขียนฟังก์ชันสมาชิก C ++ โดยใช้ a switch(idx)เพื่อเลือกสมาชิกจะเพิ่มประสิทธิภาพสำหรับดัชนีค่าคงที่เวลาคอมไพล์ แต่จะสร้าง asm ที่แตกแขนงแย่มากสำหรับดัชนีรันไทม์ ไม่มีอะไรผิดปกติswitch()สำหรับสิ่งนี้; นี่เป็นเพียงข้อผิดพลาดในการเพิ่มประสิทธิภาพที่ไม่ได้รับในคอมไพเลอร์ปัจจุบัน พวกเขาสามารถคอมไพเลอร์ Slava 'switch () ทำงานได้อย่างมีประสิทธิภาพ


วิธีแก้ปัญหา / วิธีแก้ปัญหานี้คือทำอีกวิธีหนึ่ง: ให้คลาส / โครงสร้างของคุณเป็นสมาชิกอาร์เรย์และเขียนฟังก์ชัน accessor เพื่อแนบชื่อกับองค์ประกอบเฉพาะ

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

เราสามารถดูได้ที่การส่งออก asm ที่แตกต่างกันสำหรับการใช้งานในกรณีเกี่ยวกับคอมไพเลอร์สำรวจ Godbolt นี่คือฟังก์ชัน x86-64 System V ที่สมบูรณ์โดยละเว้นคำสั่ง RET ต่อท้ายเพื่อแสดงสิ่งที่คุณจะได้รับเมื่ออยู่ในบรรทัด ARM / MIPS / อะไรก็ได้ที่คล้ายกัน

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

จากการเปรียบเทียบคำตอบของ @ Slava โดยใช้ a switch()สำหรับ C ++ ทำให้ asm เป็นเช่นนี้สำหรับดัชนีตัวแปรรันไทม์ (รหัสในลิงค์ Godbolt ก่อนหน้านี้)

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

เห็นได้ชัดว่าแย่มากเมื่อเทียบกับรุ่นการตีด้วยยูเนี่ยน C (หรือ GNU C ++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

@MM: จุดดี เป็นคำตอบสำหรับความคิดเห็นที่หลากหลายและเป็นทางเลือกสำหรับคำตอบของ Slava ฉันพูดบิตเปิดใหม่ดังนั้นอย่างน้อยก็เริ่มเป็นคำตอบสำหรับคำถามเดิม ขอบคุณที่ชี้ให้เห็น
Peter Cordes

ในขณะที่สหภาพตามประเภทเล่นสำนวนดูเหมือนว่าจะทำงานใน GCC และเสียงดังกราวในขณะที่ใช้[]ประกอบการโดยตรงในสมาชิกสหภาพการกำหนดมาตรฐานarray[index]เป็นเทียบเท่ากับ*((array)+(index))และไม่ GCC หรือเสียงดังกราวจะเชื่อถือได้ตระหนักว่าการเข้าถึงคือการเข้าถึง*((someUnion.array)+(index)) someUnionคำอธิบายเดียวที่ฉันเห็นคือsomeUnion.array[index]หรือ*((someUnion.array)+(index))ไม่ได้กำหนดโดย Standard แต่เป็นเพียงส่วนขยายยอดนิยมและ gcc / clang เลือกที่จะไม่สนับสนุนตัวที่สอง แต่ดูเหมือนว่าจะสนับสนุนตัวแรกอย่างน้อยก็ในตอนนี้
supercat

9

ใน C ++ ส่วนใหญ่เป็นพฤติกรรมที่ไม่ได้กำหนด (ขึ้นอยู่กับดัชนีใด)

จาก [expr.unary.op]:

สำหรับวัตถุประสงค์ในการคำนวณทางคณิตศาสตร์ของตัวชี้ (5.7) และการเปรียบเทียบ (5.9, 5.10) วัตถุที่ไม่ใช่องค์ประกอบอาร์เรย์ที่มีการใช้แอดเดรสด้วยวิธีนี้จะถือว่าเป็นของอาร์เรย์ที่มีองค์ประกอบประเภทTเดียว

&thing.aดังนั้นจึงถือว่านิพจน์อ้างถึงอาร์เรย์ของหนึ่งintถือว่าจึงจะอ้างถึงอาร์เรย์ของหนึ่ง

จาก [expr.sub]:

นิพจน์E1[E2]เหมือนกัน (ตามนิยาม) กับ*((E1)+(E2))

และจาก [expr.add]:

เมื่อนิพจน์ที่มีชนิดอินทิกรัลถูกเพิ่มหรือลบออกจากตัวชี้ผลลัพธ์จะมีชนิดของตัวถูกดำเนินการของตัวชี้ ถ้านิพจน์Pชี้ไปที่องค์ประกอบx[i]ของออบเจ็กต์อาร์เรย์ที่xมีnองค์ประกอบนิพจน์P + JและJ + P(ที่Jมีค่าj) จะชี้ไปที่องค์ประกอบ (อาจเป็นสมมุติฐาน) x[i + j]if 0 <= i + j <= n; มิฉะนั้นจะไม่มีการกำหนดพฤติกรรม

(&thing.a)[0]เป็นรูปแบบที่สมบูรณ์แบบเพราะ&thing.aถือเป็นอาร์เรย์ของขนาด 1 และเรากำลังใช้ดัชนีแรกนั้น นั่นคือดัชนีที่อนุญาตให้ใช้

(&thing.a)[2]ฝ่าฝืนเงื่อนไขที่ว่า0 <= i + j <= nตั้งแต่เรามีi == 0, ,j == 2 n == 1เพียงแค่สร้างตัวชี้&thing.a + 2คือพฤติกรรมที่ไม่ได้กำหนด

(&thing.a)[1]เป็นกรณีที่น่าสนใจ มันไม่ได้ละเมิดอะไรใน [expr.add] เราได้รับอนุญาตให้นำตัวชี้หนึ่งตัวไปไว้ที่จุดสิ้นสุดของอาร์เรย์ซึ่งจะเป็น ที่นี่เราจะมาดูหมายเหตุใน [basic.compound]:

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

ดังนั้นการใช้ตัวชี้&thing.a + 1จึงเป็นการกำหนดพฤติกรรม แต่การอ้างถึงมันไม่ได้กำหนดไว้เนื่องจากไม่ได้ชี้ไปที่สิ่งใด


การประเมิน (& thing.a) + 1 นั้นเกี่ยวกับกฎหมายเนื่องจากตัวชี้ที่อยู่ด้านหลังส่วนท้ายของอาร์เรย์นั้นถูกกฎหมาย การอ่านหรือเขียนข้อมูลที่จัดเก็บมีพฤติกรรมที่ไม่ได้กำหนดเปรียบเทียบกับ & thing.b กับ <,>, <=,> = คือพฤติกรรมที่ไม่ได้กำหนด (& thing.a) + 2 ผิดกฎหมายอย่างแน่นอน
gnasher729

@ gnasher729 ใช่มันคุ้มค่าที่จะชี้แจงคำตอบเพิ่มเติม
Barry

(&thing.a + 1)เป็นกรณีที่น่าสนใจที่ผมล้มเหลวในการปก +1! ... แค่สงสัยคุณอยู่ในคณะกรรมการ ISO C ++ หรือไม่?
WhiZTiM

นอกจากนี้ยังเป็นกรณีที่สำคัญมากเพราะมิฉะนั้นทุกลูปที่ใช้พอยน์เตอร์เป็นช่วงครึ่งเปิดจะเป็น UB
Jens

เกี่ยวกับการอ้างอิงมาตรฐานสุดท้าย C ++ ต้องระบุให้ดีกว่า C ที่นี่
2501

8

นี่คือพฤติกรรมที่ไม่ได้กำหนด

มีกฎมากมายใน C ++ ที่พยายามให้คอมไพเลอร์มีความหวังว่าจะเข้าใจสิ่งที่คุณกำลังทำอยู่ดังนั้นจึงสามารถให้เหตุผลเกี่ยวกับมันและปรับให้เหมาะสมได้

มีกฎเกี่ยวกับนามแฝง (การเข้าถึงข้อมูลผ่านตัวชี้สองประเภทที่แตกต่างกัน) ขอบเขตอาร์เรย์ ฯลฯ

เมื่อคุณมีตัวแปรxความจริงที่ว่ามันไม่ใช่สมาชิกของอาร์เรย์หมายความว่าคอมไพเลอร์สามารถสันนิษฐานได้ว่าไม่มี[]การเข้าถึงอาร์เรย์ตามที่สามารถแก้ไขได้ ดังนั้นจึงไม่จำเป็นต้องโหลดข้อมูลจากหน่วยความจำซ้ำทุกครั้งที่ใช้งาน เฉพาะในกรณีที่มีคนแก้ไขได้จากชื่อเท่านั้น

ดังนั้นจึงสามารถสันนิษฐานโดยคอมไพเลอร์จะไม่ได้หมายถึง(&thing.a)[1] thing.bสามารถใช้ข้อเท็จจริงนี้เพื่อจัดลำดับการอ่านและเขียนthing.bใหม่ทำให้สิ่งที่คุณต้องการให้ทำไม่ถูกต้องโดยไม่ทำให้สิ่งที่คุณบอกให้ทำจริงเป็นโมฆะ

ตัวอย่างคลาสสิกของสิ่งนี้คือการส่ง const ออกไป

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

ที่นี่คุณมักจะได้คอมไพเลอร์บอกว่า 7 แล้ว 2! = 7 จากนั้นพอยน์เตอร์ที่เหมือนกันสองตัว แม้จะมีความจริงที่ว่ามีการชี้ไปที่ptr xคอมไพเลอร์ใช้ความจริงที่xเป็นค่าคงที่เพื่อไม่ต้องรำคาญกับการอ่านเมื่อคุณขอค่าของx.

แต่เมื่อคุณรับที่อยู่xคุณบังคับให้มีอยู่ จากนั้นคุณละทิ้ง const และแก้ไข ดังนั้นตำแหน่งจริงในหน่วยความจำที่xถูกแก้ไขคอมไพเลอร์มีอิสระที่จะไม่อ่านมันเมื่ออ่านx!

คอมไพเลอร์อาจฉลาดพอที่จะคิดหาวิธีที่จะหลีกเลี่ยงการติดตามptrเพื่ออ่าน*ptrแต่มักจะไม่เป็นเช่นนั้น อย่าลังเลที่จะไปใช้งานptr = ptr+argc-1หรือเกิดความสับสนหากเครื่องมือเพิ่มประสิทธิภาพฉลาดกว่าคุณ

คุณสามารถกำหนดเองoperator[]เพื่อให้ได้สินค้าที่เหมาะสม

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

การมีทั้งสองอย่างมีประโยชน์


"ข้อเท็จจริงที่ว่าไม่ใช่สมาชิกของอาร์เรย์หมายความว่าคอมไพเลอร์สามารถสันนิษฐานได้ว่าไม่มีการเข้าถึงอาร์เรย์ตาม [] ที่สามารถแก้ไขได้" - ไม่เป็นความจริงเช่น(&thing.a)[0]อาจแก้ไขได้
MM

ฉันไม่เห็นว่าตัวอย่าง const มีส่วนเกี่ยวข้องกับคำถามอย่างไร ที่ล้มเหลวเพียงเพราะมีกฎเฉพาะที่ไม่สามารถแก้ไขวัตถุ const ได้ไม่ใช่เหตุผลอื่นใด
MM

1
@MM ไม่ใช่ตัวอย่างของการจัดทำดัชนีในโครงสร้าง แต่เป็นภาพประกอบที่ดีมากเกี่ยวกับการใช้พฤติกรรมที่ไม่ได้กำหนดเพื่ออ้างอิงบางสิ่งตามตำแหน่งที่ชัดเจนในหน่วยความจำอาจทำให้ได้ผลลัพธ์ที่แตกต่างจากที่คาดไว้เนื่องจากคอมไพเลอร์สามารถทำอย่างอื่นได้ด้วย UB มากกว่าที่คุณต้องการ
Wildcard

@MM ขออภัยไม่มีการเข้าถึงอาร์เรย์นอกเหนือจากตัวชี้ไปยังวัตถุเอง และอันที่สองเป็นเพียงตัวอย่างของผลข้างเคียงที่เห็นได้ง่ายของพฤติกรรมที่ไม่ได้กำหนด คอมไพเลอร์ปรับค่าการอ่านให้เหมาะสมที่สุดxเนื่องจากรู้ว่าคุณไม่สามารถเปลี่ยนแปลงได้ตามวิธีที่กำหนด การเพิ่มประสิทธิภาพที่คล้ายกันอาจเกิดขึ้นเมื่อคุณเปลี่ยนbผ่าน(&blah.a)[1]ถ้าคอมไพเลอร์สามารถพิสูจน์ได้ว่าไม่มีการกำหนดให้การเข้าถึงbที่สามารถปรับเปลี่ยนมัน การเปลี่ยนแปลงดังกล่าวอาจเกิดขึ้นเนื่องจากการเปลี่ยนแปลงที่ดูเหมือนไม่มีพิษมีภัยในคอมไพเลอร์รหัสรอบข้างหรืออะไรก็ตาม ดังนั้นแม้การทดสอบว่าใช้งานได้ก็ยังไม่เพียงพอ
Yakk - Adam Nevraumont

6

นี่เป็นวิธีการใช้คลาสพร็อกซีเพื่อเข้าถึงองค์ประกอบในอาร์เรย์สมาชิกตามชื่อ เป็น C ++ มากและไม่มีประโยชน์เมื่อเทียบกับฟังก์ชัน ref-return accessor ยกเว้นการกำหนดลักษณะทางไวยากรณ์ สิ่งนี้จะทำให้ตัว->ดำเนินการเข้าถึงองค์ประกอบในฐานะสมาชิกมากเกินไปดังนั้นเพื่อให้เป็นที่ยอมรับเราจำเป็นต้องไม่ชอบทั้งไวยากรณ์ของ accessors ( d.a() = 5;) รวมทั้งอดทนต่อการใช้->กับออบเจ็กต์ที่ไม่ใช่พอยน์เตอร์ ฉันคาดว่าสิ่งนี้อาจสร้างความสับสนให้กับผู้อ่านที่ไม่คุ้นเคยกับรหัสดังนั้นนี่อาจเป็นเคล็ดลับที่เรียบร้อยมากกว่าสิ่งที่คุณต้องการนำไปผลิต

โครงสร้างDataในรหัสนี้ยังรวมถึงโอเวอร์โหลดสำหรับตัวดำเนินการตัวห้อยเพื่อเข้าถึงองค์ประกอบที่จัดทำดัชนีภายในarสมาชิกอาร์เรย์เช่นเดียวกับbeginและendฟังก์ชันสำหรับการทำซ้ำ นอกจากนี้สิ่งเหล่านี้มีมากเกินไปด้วยเวอร์ชันที่ไม่ใช่ const และ const ซึ่งฉันรู้สึกว่าจำเป็นต้องรวมไว้เพื่อความสมบูรณ์

เมื่อData's ->ใช้ในการเข้าถึงองค์ประกอบตามชื่อ (เช่นนี้my_data->b = 5;) ซึ่งเป็นProxyวัตถุที่ถูกส่งกลับ ดังนั้นเนื่องจากProxyrvalue นี้ไม่ใช่ตัวชี้ตัว->ดำเนินการของตัวเองจึงถูกเรียกโดยอัตโนมัติซึ่งจะส่งคืนตัวชี้ให้กับตัวเอง ด้วยวิธีนี้Proxyอ็อบเจ็กต์จะถูกสร้างอินสแตนซ์และยังคงใช้ได้ในระหว่างการประเมินนิพจน์เริ่มต้น

Contruction ของProxyวัตถุ populates สมาชิก 3 อ้างอิงa, bและcตามตัวชี้ผ่านในคอนสตรัคซึ่งจะถือว่าจุดการบัฟเฟอร์ที่มีอย่างน้อย 3 Tค่าที่มีประเภทจะได้รับเป็นพารามิเตอร์แม่แบบ ดังนั้นแทนที่จะใช้การอ้างอิงที่มีชื่อซึ่งเป็นสมาชิกของDataคลาสสิ่งนี้จะช่วยประหยัดหน่วยความจำโดยการเติมข้อมูลอ้างอิงที่จุดเข้าถึง (แต่น่าเสียดายที่ใช้->ไม่ใช่ตัว.ดำเนินการ)

เพื่อทดสอบว่าเครื่องมือเพิ่มประสิทธิภาพของคอมไพเลอร์กำจัด indirection ทั้งหมดที่แนะนำโดยการใช้งานได้ดีเพียงProxyใดโค้ดด้านล่างนี้มี 2 เวอร์ชันของmain(). #if 1รุ่นใช้->และ[]ผู้ประกอบการและ#if 0รุ่นที่มีประสิทธิภาพเทียบเท่าชุดของขั้นตอนการ แต่เพียงโดยการเข้าถึงโดยตรงData::arแต่เพียงโดยการเข้าถึงโดยตรง

Nci()ฟังก์ชั่นสร้างค่าจำนวนเต็มรันไทม์สำหรับการเริ่มต้นองค์ประกอบมากมายซึ่งช่วยป้องกันการเพิ่มประสิทธิภาพจากเพียงเสียบค่าคงที่โดยตรงในแต่ละstd::cout <<โทร

สำหรับ gcc 6.2 โดยใช้ -O3 ทั้งสองเวอร์ชันmain()จะสร้างแอสเซมบลีเดียวกัน (สลับระหว่าง#if 1และ#if 0ก่อนหน้าmain()เพื่อเปรียบเทียบ): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

ดี โหวตเพิ่มขึ้นเป็นหลักเพราะคุณพิสูจน์แล้วว่าสิ่งนี้ช่วยเพิ่มประสิทธิภาพ BTW คุณสามารถทำสิ่งนั้นได้ง่ายขึ้นมากโดยการเขียนฟังก์ชันที่เรียบง่ายไม่ใช่ทั้งmain()ฟังก์ชันจับเวลา! เช่นint getb(Data *d) { return (*d)->b; }คอมไพล์เป็น just mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ) (ใช่Data &dจะทำให้ไวยากรณ์ง่ายขึ้น แต่ฉันใช้ตัวชี้แทนการอ้างอิงเพื่อเน้นความแปลกประหลาดของการโอเวอร์โหลด->ด้วยวิธีนี้)
Peter Cordes

ยังไงก็เท่ดีนะ แนวคิดอื่น ๆ เช่นint tmp[] = { a, b, c}; return tmp[idx];อย่าเพิ่มประสิทธิภาพออกไปดังนั้นจึงเป็นเรื่องที่เรียบร้อย
Peter Cordes

อีกเหตุผลหนึ่งที่ฉันพลาดoperator.ใน C ++ 17
Jens

2

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

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

สำหรับเวอร์ชัน C ++ เท่านั้นคุณอาจต้องการใช้static_assertเพื่อตรวจสอบว่าstruct dataมีเค้าโครงมาตรฐานและอาจมีข้อยกเว้นในดัชนีที่ไม่ถูกต้องแทน


1

ผิดกฎหมาย แต่มีวิธีแก้ปัญหาดังนี้

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

ตอนนี้คุณสามารถทำดัชนี v:


6
โปรเจ็กต์ c ++ จำนวนมากคิดว่าการลดระดับลงทั่วทุกแห่งนั้นดี เรายังไม่ควรสั่งสอนการปฏิบัติที่ไม่ดี
StoryTeller - Unslander Monica

2
สหภาพช่วยแก้ปัญหานามแฝงที่เข้มงวดในทั้งสองภาษา แต่การพิมพ์การลงโทษผ่านสหภาพแรงงานนั้นใช้ได้เฉพาะใน C เท่านั้นไม่ใช่ใน C ++
Lundin

1
ถึงกระนั้นฉันก็ไม่แปลกใจถ้ามันใช้งานได้ 100% ของคอมไพเลอร์ c ++ ทั้งหมด เคย.
Sven Nilsson

1
คุณสามารถทดลองใช้ใน gcc โดยเปิดการตั้งค่าเครื่องมือเพิ่มประสิทธิภาพที่เข้มงวดที่สุด
Lundin

1
@Lundin: การกดประเภทยูเนี่ยนถูกกฎหมายในGNU C ++ เป็นส่วนขยายของ ISO C ++ ดูเหมือนจะไม่ได้ระบุไว้อย่างชัดเจนในคู่มือแต่ฉันค่อนข้างแน่ใจเกี่ยวกับเรื่องนี้ อย่างไรก็ตามคำตอบนี้ต้องอธิบายว่าถูกต้องตรงไหนและไม่ถูกต้อง
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.