โหลดพฤติกรรมที่ไม่ได้กำหนดและจุดลำดับใหม่


84

พิจารณาหัวข้อนี้เป็นภาคต่อของหัวข้อต่อไปนี้:

งวดก่อนหน้า
พฤติกรรมที่ไม่ได้กำหนดและจุดลำดับ

มาทบทวนสำนวนที่ตลกและซับซ้อนนี้กันอีกครั้ง(วลีตัวเอียงนำมาจากหัวข้อด้านบน * smile *):

i += ++i;

เรากล่าวว่าสิ่งนี้ก่อให้เกิดพฤติกรรมที่ไม่ได้กำหนด ผมเข้าใจว่าเมื่อพูดแบบนี้เราคิดว่าโดยปริยายประเภทของการiเป็นหนึ่งในตัวชนิด

เกิดอะไรขึ้นถ้าประเภทของการiเป็นผู้ใช้กำหนดชนิดใด? บอกว่าประเภทของมันคือIndexสิ่งที่กำหนดไว้ในโพสต์นี้ (ดูด้านล่าง) มันจะยังคงเรียกใช้พฤติกรรมที่ไม่ได้กำหนดหรือไม่?

ถ้าใช่เพราะเหตุใด มันไม่เทียบเท่ากับการเขียนi.operator+=(i.operator++());หรือแม้แต่ไวยากรณ์ที่เรียบง่ายกว่า i.add(i.inc());? หรือพวกเขาเรียกร้องพฤติกรรมที่ไม่ได้กำหนดมากเกินไป?

ถ้าไม่ทำไมไม่? ท้ายที่สุดวัตถุiจะถูกแก้ไขสองครั้งระหว่างจุดลำดับที่ต่อเนื่องกัน โปรดระลึกถึงหลักการง่ายๆ: นิพจน์สามารถแก้ไขค่าของอ็อบเจ็กต์ได้เพียงครั้งเดียวระหว่าง "ลำดับจุด" ที่ต่อเนื่องกันและถ้า i += ++iเป็นนิพจน์ก็จะต้องเรียกใช้พฤติกรรมที่ไม่ได้กำหนดหากเป็นเช่นนั้นจะมีค่าเทียบเท่าi.operator+=(i.operator++());และ i.add(i.inc());ต้องเรียกใช้พฤติกรรมที่ไม่ได้กำหนดซึ่ง ดูเหมือนจะไม่จริง! (เท่าที่ฉันเข้าใจ)

หรือi += ++iไม่ใช่นิพจน์ที่ขึ้นต้นด้วย? ถ้าเป็นเช่นนั้นคำจำกัดความของนิพจน์คืออะไร?

หากเป็นนิพจน์และในเวลาเดียวกันพฤติกรรมของมันก็ถูกกำหนดไว้อย่างดีเช่นกันแสดงว่าจำนวนจุดลำดับที่เกี่ยวข้องกับนิพจน์นั้นขึ้นอยู่กับประเภทของตัวถูกดำเนินการที่เกี่ยวข้องกับนิพจน์ ฉันถูกต้อง (แม้บางส่วน) หรือไม่?


แล้วสำนวนนี้ล่ะ?

//Consider two cases:
//1. If a is an array of a built-in type
//2. If a is user-defined type which overloads the subscript operator!

a[++i] = i; //Taken from the previous topic. But here type of `i` is Index.

คุณต้องคำนึงถึงสิ่งนี้ด้วยในการตอบสนองของคุณ (ถ้าคุณรู้พฤติกรรมของมันอย่างแน่นอน) :-)


คือ

++++++i;

กำหนดไว้อย่างดีใน C ++ 03? ท้ายที่สุดนี่คือสิ่งนี้

((i.operator++()).operator++()).operator++();

class Index
{
    int state;

    public:
        Index(int s) : state(s) {}
        Index& operator++()
        {
            state++;
            return *this;
        }
        Index& operator+=(const Index & index)
        {
            state+= index.state;
            return *this;
        }
        operator int()
        {
            return state;
        }
        Index & add(const Index & index)
        {
            state += index.state;
            return *this;
        }
        Index & inc()
        {
            state++;
            return *this;
        }
};

13
+1 คำถามที่ยอดเยี่ยมซึ่งเป็นแรงบันดาลใจให้กับคำตอบที่ยอดเยี่ยม ฉันรู้สึกว่าฉันควรจะบอกว่ามันยังคงเป็นรหัสที่น่ากลัวซึ่งควรได้รับการปรับโครงสร้างใหม่เพื่อให้อ่านได้ง่ายขึ้น แต่คุณคงรู้อยู่แล้ว :)
Philip Potter

4
@ คำถามคืออะไรใครบอกว่าเหมือนกัน? หรือใครบอกว่าไม่เหมือนกัน? มันไม่ได้ขึ้นอยู่กับว่าคุณใช้มันอย่างไร? (หมายเหตุ: ฉันสมมติว่าประเภทเป็นประเภทที่sผู้ใช้กำหนดเอง!)
นาวาซ

5
ฉันไม่เห็นว่ามีการแก้ไขวัตถุสเกลาร์สองครั้งระหว่างจุดลำดับสองจุด ...
Johannes Schaub - litb

3
@ โจฮันเนส: มันเกี่ยวกับวัตถุสเกลาร์ มันคืออะไร? ฉันสงสัยว่าทำไมฉันไม่เคยได้ยินมาก่อน อาจเป็นเพราะบทช่วยสอน / C ++ - คำถามที่พบบ่อยไม่ได้กล่าวถึงหรือไม่เน้น? แตกต่างจากวัตถุในตัวหรือไม่?
Nawaz

3
@ ฟิลลิป: แน่นอนฉันจะไม่เขียนโค้ดแบบนี้ในชีวิตจริง ในความเป็นจริงไม่มีโปรแกรมเมอร์คนไหนที่จะเขียนมันได้ โดยปกติคำถามเหล่านี้จะถูกคิดค้นขึ้นเพื่อให้เราเข้าใจธุรกิจทั้งหมดของพฤติกรรมที่ไม่ได้กำหนดและลำดับประเด็นได้ดีขึ้น! :-)
Nawaz

คำตอบ:


48

ดูเหมือนว่ารหัส

i.operator+=(i.operator ++());

ทำงานได้ดีอย่างสมบูรณ์แบบเกี่ยวกับจุดลำดับ ส่วน 1.9.17 ของมาตรฐาน ISO C ++ กล่าวเกี่ยวกับจุดลำดับและการประเมินฟังก์ชัน:

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

ตัวอย่างเช่นสิ่งนี้จะบ่งชี้ว่าi.operator ++()เป็นพารามิเตอร์ที่จะoperator +=มีจุดลำดับหลังจากการประเมิน กล่าวโดยย่อเนื่องจากตัวดำเนินการที่โอเวอร์โหลดเป็นฟังก์ชันจึงใช้กฎการจัดลำดับตามปกติ

เป็นคำถามที่ดีมาก! ฉันชอบมากที่คุณบังคับให้ฉันเข้าใจความแตกต่างทั้งหมดของภาษาที่ฉันคิดว่าฉันรู้อยู่แล้ว (และคิดว่าฉันคิดว่าฉันรู้) :-)


12

http://www.eelis.net/C++/analogliterals.xhtml ตัวอักษรอนาล็อกอยู่ในความคิดของฉัน

  unsigned int c = ( o-----o
                     |     !
                     !     !
                     !     !
                     o-----o ).area;

  assert( c == (I-----I) * (I-------I) );

  assert( ( o-----o
            |     !
            !     !
            !     !
            !     !
            o-----o ).area == ( o---------o
                                |         !
                                !         !
                                o---------o ).area );

มีคำถามคือ ++++++ ผม; กำหนดไว้อย่างดีใน C ++ 03?
ยากล่อมประสาททางอุตสาหกรรม

11

ตามที่คนอื่น ๆ กล่าวไว้i += ++iตัวอย่างของคุณใช้งานได้กับประเภทที่ผู้ใช้กำหนดเองเนื่องจากคุณกำลังเรียกใช้ฟังก์ชันและฟังก์ชันประกอบด้วยจุดลำดับ

ในทางกลับกันa[++i] = iไม่โชคดีนักที่สมมติว่าaเป็นประเภทอาร์เรย์พื้นฐานของคุณหรือแม้แต่ผู้ใช้กำหนดเอง ปัญหาที่คุณพบคือเราไม่รู้ว่าส่วนใดของนิพจน์ที่มีการiประเมินก่อน อาจเป็นไปได้ว่า++iมีการประเมินส่งผ่านไปยังoperator[](หรือเวอร์ชันดิบ) เพื่อดึงข้อมูลวัตถุที่นั่นจากนั้นค่าของการiรับส่งผ่านไปยังสิ่งนั้น (ซึ่งหลังจากที่ฉันถูกเพิ่มขึ้น) ในทางกลับกันอาจจะประเมินด้านหลังก่อนเก็บไว้สำหรับการมอบหมายงานในภายหลังจากนั้น++iจึงประเมินส่วนนั้น


ดังนั้น ... ผลลัพธ์จึงไม่ระบุแทนที่จะเป็น UB เนื่องจากไม่ได้ระบุลำดับการประเมินนิพจน์?
Philip Potter

@ ฟิลิป: ไม่ระบุหมายความว่าเราคาดหวังให้คอมไพเลอร์ระบุพฤติกรรมในขณะที่ไม่ได้กำหนดไว้ไม่มีภาระผูกพันดังกล่าว ฉันคิดว่ามันไม่ได้กำหนดไว้ที่นี่เพื่อให้คอมไพเลอร์มีพื้นที่มากขึ้นสำหรับการปรับให้เหมาะสม
Matthieu M.

@ โนอาห์: ฉันยังโพสต์ตอบกลับ โปรดตรวจสอบและแจ้งให้เราทราบความคิดของคุณ :-)
Nawaz

1
@ ฟิลิป: ผลลัพธ์คือ UB เนื่องจากกฎใน 5/4: "ข้อกำหนดของย่อหน้านี้จะต้องเป็นไปตามสำหรับแต่ละลำดับที่อนุญาตของนิพจน์ย่อยของนิพจน์แบบเต็มมิฉะนั้นพฤติกรรมจะไม่ได้กำหนด" หากคำสั่งที่อนุญาตทั้งหมดมีจุดลำดับระหว่างการแก้ไข++iและการอ่านiบน RHS ของงานที่มอบหมายคำสั่งนั้นจะไม่ระบุ เนื่องจากหนึ่งในคำสั่งที่อนุญาตทำให้สองสิ่งนี้ไม่มีจุดลำดับที่แทรกแซงพฤติกรรมจึงไม่ได้กำหนดไว้
Steve Jessop

1
@ ฟิลิป: ไม่เพียง แต่กำหนดพฤติกรรมที่ไม่ระบุว่าเป็นพฤติกรรมที่ไม่ได้กำหนด อีกครั้งถ้าช่วงของพฤติกรรมที่ไม่ระบุรายละเอียดรวมถึงบางอย่างที่จะไม่ได้กำหนด, แล้วพฤติกรรมโดยรวมจะไม่ได้กำหนด ถ้าช่วงของพฤติกรรมที่ไม่ระบุจะถูกกำหนดไว้ในความเป็นไปได้ทั้งหมดแล้วพฤติกรรมโดยรวมที่ไม่ระบุ แต่คุณขวาในประเด็นที่สองผมคิดว่าการที่ผู้ใช้กำหนดและในตัวa i
Steve Jessop

8

ฉันคิดว่ามันถูกกำหนดไว้อย่างดี:

จากร่างมาตรฐาน C ++ (n1905) §1.9 / 16:

"นอกจากนี้ยังมีจุดลำดับหลังจากการคัดลอกค่าที่ส่งคืนและก่อนการดำเนินการของนิพจน์ใด ๆ นอกฟังก์ชัน 13) หลายบริบทใน C ++ ทำให้เกิดการประเมินการเรียกฟังก์ชันแม้ว่าจะไม่มีไวยากรณ์การเรียกฟังก์ชันที่เกี่ยวข้องปรากฏในหน่วยการแปล [ ตัวอย่าง : การประเมินนิพจน์ใหม่เรียกใช้ฟังก์ชันการจัดสรรและคอนสตรัคเตอร์ตั้งแต่หนึ่งรายการขึ้นไปโปรดดูที่ 5.3.4 สำหรับอีกตัวอย่างหนึ่งการเรียกใช้ฟังก์ชันการแปลง (12.3.2) อาจเกิดขึ้นในบริบทที่ไม่มีไวยากรณ์การเรียกฟังก์ชันปรากฏขึ้น - end ตัวอย่าง ] จุดลำดับที่รายการฟังก์ชันและฟังก์ชันออก (ตามที่อธิบายไว้ข้างต้น) เป็นคุณลักษณะของการเรียกใช้ฟังก์ชันตามที่ประเมินไม่ว่าจะเป็นไวยากรณ์ของนิพจน์ใดก็ตามที่เรียกใช้ฟังก์ชันอาจเป็น "

สังเกตส่วนที่ฉันเป็นตัวหนา ซึ่งหมายความว่ามีจุดลำดับที่แน่นอนหลังจากการเรียกใช้ฟังก์ชันเพิ่มขึ้น ( i.operator ++()) แต่ก่อนการเรียกใช้การกำหนดแบบผสม ( i.operator+=)


6

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

a[++i] = i;

กรณีที่ 1:

ถ้าaเป็นอาร์เรย์ประเภทบิวท์อิน แล้วสิ่งที่โนอาห์พูดนั้นถูกต้อง นั่นคือ,

a [++ i] = ฉันไม่โชคดีนักที่สมมติว่า a เป็นประเภทอาร์เรย์พื้นฐานของคุณ หรือแม้แต่ผู้ใช้กำหนดเอง . ปัญหาที่คุณพบคือเราไม่รู้ว่าส่วนใดของนิพจน์ที่มี i ได้รับการประเมินก่อน

ดังนั้นจึงa[++i]=iเรียกใช้พฤติกรรมที่ไม่ได้กำหนดหรือผลลัพธ์ไม่ได้ระบุ ไม่ว่าจะเป็นอะไรก็ไม่ได้กำหนดไว้อย่างชัดเจน!

PS: ในใบเสนอราคาข้างต้น ขีดฆ่า เป็นของฉันแน่นอน

กรณีที่ 2:

ถ้าaเป็นอ็อบเจ็กต์ประเภทที่ผู้ใช้กำหนดเองซึ่งโอเวอร์โหลดoperator[]มีอีกสองกรณี

  1. หากประเภทการส่งคืนของoperator[]ฟังก์ชันโอเวอร์โหลดเป็นประเภทที่มีอยู่แล้วa[++i]=iให้เรียกใช้พฤติกรรมที่ไม่ได้กำหนดอีกครั้งมิฉะนั้นผลลัพธ์จะไม่ระบุ
  2. แต่ถ้าประเภทการกลับมาของมากเกินไปoperator[]ฟังก์ชั่นที่ผู้ใช้กำหนดชนิดแล้วลักษณะการทำงานของa[++i] = iดีที่กำหนดไว้ (เท่าที่ผมเข้าใจ) เนื่องจากในกรณีนี้a[++i]=iจะเทียบเท่ากับการเขียนซึ่งเป็นเช่นเดียวกับa.operator[](++i).operator=(i); a[++i].operator=(i);นั่นคือการกำหนดoperator=จะถูกเรียกใช้บนอ็อบเจ็กต์ที่ส่งคืนa[++i]ซึ่งดูเหมือนว่าจะมีการกำหนดไว้อย่างดีเนื่องจากเมื่อa[++i]ส่งคืนเวลา++iแล้วได้รับการประเมินแล้วจากนั้นฟังก์ชันการเรียกอ็อบเจ็กต์ที่ส่งคืนoperator=จะส่งค่าที่อัปเดตiเป็นอาร์กิวเมนต์ โปรดสังเกตว่ามีจุดลำดับระหว่างการเรียกทั้งสองนี้ และไวยากรณ์ช่วยให้มั่นใจได้ว่าไม่มีการแข่งขันระหว่างสองสายนี้และoperator[]จะได้รับการเรียกใช้ก่อนและต่อเนื่องกันอาร์กิวเมนต์++iผ่านเข้ามาจะได้รับการประเมินก่อน

ลองนึกถึงสิ่งนี้someInstance.Fun(++k).Gun(10).Sun(k).Tun();ในการเรียกใช้ฟังก์ชันที่ต่อเนื่องกันแต่ละครั้งจะส่งคืนอ็อบเจ็กต์ของประเภทที่ผู้ใช้กำหนดเอง สำหรับฉันสถานการณ์นี้ดูเหมือนมากกว่านี้: eat(++k);drink(10);sleep(k)เนื่องจากในทั้งสองสถานการณ์มีจุดลำดับอยู่หลังจากการเรียกใช้ฟังก์ชันแต่ละครั้ง

กรุณาแก้ไขฉันถ้าฉันผิด :-)


1
@Nawaz k++และไม่kถูกคั่นด้วยจุดลำดับ ทั้งคู่สามารถประเมินก่อนหรือประเมินก็ได้ ภาษาเพียงต้องการให้มีการประเมินก่อนไม่ว่า'ข้อโต้แย้ง s จะมีการประเมินก่อนของข้อโต้แย้ง ฉันกำลังอธิบายสิ่งเดียวกันนี้อีกครั้งโดยไม่สามารถให้ข้อมูลอ้างอิงได้ดังนั้นเราจะไม่ดำเนินการต่อจากที่นี่ SunFunFunSunFunSun
Philip Potter

1
@Nawaz: เพราะไม่มีอะไรที่กำหนดจุดลำดับแยกพวกเขา มีลำดับจุดก่อนและหลังการSunดำเนินการ แต่Funอาร์กิวเมนต์++kอาจได้รับการประเมินก่อนหรือหลังนั้น มีลำดับจุดก่อนและหลังการFunดำเนินการ แต่Sunอาร์กิวเมนต์kอาจได้รับการประเมินก่อนหรือหลังนั้น ดังนั้นเป็นไปได้กรณีหนึ่งคือทั้งสองkและ++kมีการประเมินก่อนการอย่างใดอย่างหนึ่งSunหรือFunได้รับการประเมินและเพื่อให้ทั้งสองก่อนที่จะจุดลำดับฟังก์ชั่นการโทรและดังนั้นจึงไม่มีจุดลำดับแยกและk ++k
Philip Potter

1
@ ฟิลิป: ฉันพูดซ้ำ: สถานการณ์นี้แตกต่างจากeat(i++);drink(10);sleep(i);อย่างไร? ... ถึงตอนนี้จะบอกว่าi++อาจจะประเมินก่อนหรือหลังก็ได้?
Nawaz

1
@Nawaz: ฉันจะทำให้ตัวเองชัดเจนขึ้นได้อย่างไร? ในความสนุกสนาน / Sun ตัวอย่างเช่นมีไม่มีจุดลำดับระหว่างและk ++kในการกินเช่นเครื่องดื่ม / มีคือเป็นจุดลำดับระหว่างและi i++
Philip Potter

3
@ ฟิลิป: นั่นไม่สมเหตุสมผลเลย ระหว่าง Fun () และ Sun () มีจุดลำดับ แต่ระหว่างอาร์กิวเมนต์ไม่มีจุดลำดับ มันเหมือนกับการพูดระหว่างeat()และsleep()มีอยู่จุดลำดับ แต่ระหว่างมีอาร์กิวเมนต์ไม่มีแม้แต่อันเดียว อาร์กิวเมนต์ของการเรียกใช้ฟังก์ชันสองรายการที่คั่นด้วยจุดลำดับจะอยู่ในจุดลำดับเดียวกันได้อย่างไร
Nawaz
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.