ฉันควรเริ่มต้น C structs ผ่านพารามิเตอร์หรือโดยค่าตอบแทน? [ปิด]


33

บริษัท ที่ฉันทำงานอยู่นั้นกำลังเริ่มต้นโครงสร้างข้อมูลทั้งหมดผ่านฟังก์ชันเตรียมใช้งานเช่น:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

ฉันได้รับแรงผลักดันบางอย่างพยายามเริ่มต้นโครงสร้างของฉันเช่นนี้:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

มีข้อได้เปรียบอย่างใดอย่างหนึ่งมากกว่าที่อื่น ๆ ?
ฉันควรทำสิ่งใดและฉันจะทำให้มันเป็นแนวปฏิบัติที่ดีขึ้นได้อย่างไร


4
@gnat นี่เป็นคำถามที่ชัดเจนเกี่ยวกับการเริ่มต้น struct หัวข้อนั้นเป็นเพียงเหตุผลเดียวที่ฉันต้องการนำไปใช้กับการตัดสินใจออกแบบนี้
เทรเวอร์ Hickey

2
@Jefffrey เราอยู่ใน C ดังนั้นเราจึงไม่มีวิธีการจริงๆ มันไม่ได้เป็นชุดของค่าโดยตรงเสมอไป บางครั้งการเริ่มต้นโครงสร้างคือการรับค่า (อย่างใด) และดำเนินการตรรกะบางอย่างเพื่อเริ่มต้นโครงสร้าง
เทรเวอร์ Hickey

1
@ JacquesB ฉันได้รับ "องค์ประกอบทุกอย่างที่คุณสร้างจะแตกต่างจากที่อื่น ๆ มีฟังก์ชั่นเริ่มต้น () ที่ใช้ที่อื่นสำหรับโครงสร้างการพูดทางเทคนิคเรียกมันว่าตัวสร้างจะทำให้เข้าใจผิด"
เทรเวอร์ Hickey

1
@TrevorHickey InitializeFoo()เป็นตัวสร้าง ความแตกต่างเพียงอย่างเดียวจากคอนสตรัคเตอร์ C ++ คือthisตัวชี้จะถูกส่งผ่านอย่างชัดเจนมากกว่าโดยปริยาย รหัสที่คอมไพล์InitializeFoo()แล้วและ C ++ ที่เกี่ยวข้องFoo::Foo()นั้นเหมือนกันทุกประการ
cmaster

2
ตัวเลือกที่ดีกว่า: หยุดใช้ C มากกว่า C ++ AutoWin
Thomas Eding

คำตอบ:


25

ในวิธีที่ 2 คุณจะไม่มี Foo ที่เริ่มต้นได้ การวางสิ่งก่อสร้างทั้งหมดไว้ในที่แห่งเดียวดูเหมือนว่าเป็นสถานที่ที่สมเหตุสมผลและชัดเจนกว่า

แต่ ... วิธีที่ 1 ไม่ได้เลวร้ายและมักใช้ในหลาย ๆ พื้นที่ (มีการอภิปรายถึงวิธีที่ดีที่สุดในการพึ่งพาการฉีดไม่ว่าจะเป็นการฉีดคุณสมบัติเช่นเดียวกับวิธีที่ 1 หรือการฉีดคอนสตรัคเตอร์เช่น 2) . ไม่ผิด

ดังนั้นหากไม่ผิดและส่วนที่เหลือของ บริษัท ใช้วิธี # 1 คุณควรเหมาะสมกับ codebase ที่มีอยู่และไม่พยายามทำให้ยุ่งเหยิงโดยการแนะนำรูปแบบใหม่ นี่เป็นปัจจัยที่สำคัญที่สุดในการเล่นที่นี่เล่นกับเพื่อนใหม่ของคุณและอย่าพยายามเป็นเกล็ดหิมะพิเศษที่ทำสิ่งต่าง ๆ


ตกลงดูเหมือนว่าสมเหตุสมผล ฉันรู้สึกว่าการเริ่มต้นวัตถุโดยไม่สามารถดูว่าชนิดของข้อมูลเริ่มต้นนั้นจะนำไปสู่ความสับสน ฉันพยายามทำตามแนวคิดของ data in / data out เพื่อสร้างโค้ดที่สามารถคาดเดาได้และทดสอบได้ การทำวิธีอื่นดูเหมือนว่าจะเพิ่มการเชื่อมต่อเนื่องจากไฟล์ต้นฉบับของโครงสร้างของฉันต้องการการอ้างอิงพิเศษเพื่อดำเนินการเริ่มต้น แม้ว่าคุณจะถูกที่ฉันไม่ต้องการที่จะเขย่าเรือเว้นแต่วิธีหนึ่งเป็นที่ต้องการอย่างมากมากกว่าอีก
เทรเวอร์ Hickey

4
@TrevorHickey: จริง ๆ แล้วฉันจะบอกว่ามีสองความแตกต่างที่สำคัญระหว่างตัวอย่างที่คุณให้ - (1) ในหนึ่งฟังก์ชั่นจะถูกส่งผ่านตัวชี้ไปยังโครงสร้างเพื่อเริ่มต้นและอื่น ๆ ก็ส่งกลับโครงสร้างเริ่มต้น; (2) ในหนึ่งพารามิเตอร์เริ่มต้นจะถูกส่งผ่านไปยังฟังก์ชั่นและอื่น ๆ ที่พวกเขามีความหมาย คุณดูเหมือนจะถามเพิ่มเติมเกี่ยวกับ (2) แต่คำตอบที่นี่มุ่งเน้นที่ (1) คุณอาจต้องการชี้แจงว่า - ฉันสงสัยว่าคนส่วนใหญ่จะแนะนำลูกผสมทั้งสองโดยใช้พารามิเตอร์ที่ชัดเจนและตัวชี้:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
วิธีแรกจะนำไปสู่โครงสร้าง "เริ่มต้นครึ่ง" ได้Fooอย่างไร วิธีแรกยังทำการเริ่มต้นทั้งหมดในที่เดียว (หรือที่คุณกำลังพิจารณายกเลิกการเริ่มต้นFoostruct จะเป็น "ครึ่งเริ่มต้น"?)
jamesdlin

1
@jamesdlin ในกรณีที่สร้าง Foo ขึ้นมาและ InitialiseFoo พลาดไปโดยไม่ได้ตั้งใจ มันเป็นเพียงคำพูดเพื่ออธิบายการเริ่มต้น 2 เฟสโดยไม่ต้องพิมพ์คำอธิบายที่ยาว ฉันคิดว่าคนประเภทนักพัฒนาที่มีประสบการณ์จะเข้าใจ
gbjbaanb

22

ทั้งสองวิธีจะรวมรหัสการเริ่มต้นไว้ในการเรียกใช้ฟังก์ชันเดียว จนถึงตอนนี้ดีมาก

อย่างไรก็ตามมีสองประเด็นเกี่ยวกับวิธีที่สอง:

  1. อันที่สองไม่ได้สร้างวัตถุที่เกิดขึ้นจริงมันเริ่มต้นวัตถุอื่นในสแต็คซึ่งจะถูกคัดลอกไปยังวัตถุสุดท้าย นี่คือเหตุผลที่ฉันจะเห็นวิธีการที่สองเป็นรองเล็กน้อย การกดกลับที่คุณได้รับอาจเนื่องมาจากสำเนาที่ไม่เกี่ยวข้องนี้

    สิ่งนี้ยิ่งแย่ลงเมื่อคุณได้รับคลาสDerivedจากFoo(structs ส่วนใหญ่ใช้สำหรับการวางแนววัตถุใน C): ด้วยวิธีที่สองฟังก์ชันConstructDerived()จะเรียกใช้ConstructFoo()คัดลอกFooวัตถุชั่วคราวที่เป็นผลลัพธ์ลงในสล็อตซูเปอร์Derivedคลาสของวัตถุ เสร็จสิ้นการเริ่มต้นของDerivedวัตถุนั้น เพียงเพื่อให้วัตถุที่เป็นผลลัพธ์ถูกคัดลอกมาอีกครั้งเมื่อกลับมา เพิ่มเลเยอร์ที่สามและสิ่งทั้งหมดกลายเป็นเรื่องไร้สาระอย่างสมบูรณ์

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


ในที่สุดไม่ได้ทั้งหมดstructsเป็นชั้นเรียนที่เต็มเปี่ยม บางstructsอย่างมีประสิทธิภาพเพียงรวมกลุ่มของตัวแปรเข้าด้วยกันโดยไม่มีข้อ จำกัด ภายในกับค่าของตัวแปรเหล่านี้ typedef struct Point { int x, y; } Point;จะเป็นตัวอย่างที่ดีของสิ่งนี้ สำหรับการใช้งานฟังก์ชั่นการเริ่มต้นเหล่านี้ดูเหมือนจะเกินความจริง ในกรณีเหล่านี้ไวยากรณ์ตัวอักษรผสมอาจสะดวก (เป็น C99):

Point = { .x = 7, .y = 9 };

หรือ

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}

5
ฉันไม่คิดว่าสำเนาจะเป็นปัญหาเนื่องจากen.wikipedia.org/wiki/Copy_elision
Trevor Hickey

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

3
ทุกคนที่ใช้คอมไพเลอร์ควรคาดหวังว่าโค้ดที่พวกเขาเขียนจะถูกแปลงโดยคอมไพเลอร์ เว้นแต่ว่าพวกเขากำลังใช้ล่าม C ฮาร์ดแวร์รหัสที่พวกเขาเขียนจะไม่เป็นรหัสที่พวกเขาดำเนินการแม้ว่ามันจะง่ายที่จะเชื่อ หากพวกเขาเข้าใจคอมไพเลอร์ของพวกเขาพวกเขาจะเข้าใจการเลือกและมันก็ไม่ต่างไปกว่าการint x = 3;ไม่เก็บสตริงxไว้ในไบนารี ที่อยู่และจุดรับมรดกนั้นดี ความล้มเหลวที่เกิดขึ้นของ elision นั้นเป็นเรื่องโง่
Yakk

@Yakk: ประวัติศาสตร์ C ถูกประดิษฐ์ขึ้นเพื่อทำหน้าที่เป็นรูปแบบของภาษาแอสเซมบลีระดับสูงสำหรับการเขียนโปรแกรมระบบ ในปีที่ผ่านมาตัวตนของมันได้กลายเป็นมืดมนมากขึ้น บางคนต้องการให้เป็นภาษาแอปพลิเคชั่นที่ได้รับการปรับปรุง แต่เนื่องจากภาษาแอสเซมบลีระดับสูงไม่มีรูปแบบที่ดีกว่าปรากฏขึ้น ฉันเห็นว่าไม่มีอะไรผิดปกติกับแนวคิดที่ว่าโค้ดโปรแกรมที่เขียนขึ้นอย่างดีควรมีพฤติกรรมอย่างน้อย decently แม้เมื่อคอมไพล์ด้วยการปรับให้เหมาะสมน้อยที่สุด แต่เพื่อให้การทำงานจริงจะต้องใช้ C เพิ่มบางสิ่งที่มันขาดมานาน
supercat

@Yakk: มีคำสั่งตัวอย่างเช่นซึ่งจะบอกคอมไพเลอร์ "ตัวแปรต่อไปนี้อาจถูกเก็บอย่างปลอดภัยในการลงทะเบียนในช่วงรหัสต่อไปนี้" เช่นเดียวกับวิธีการคัดลอกบล็อกชนิดอื่นนอกเหนือจากที่unsigned charจะช่วยให้การเพิ่มประสิทธิภาพที่ กฎนามแฝงที่เข้มงวดจะไม่เพียงพอในขณะที่ทำให้ความคาดหวังของโปรแกรมเมอร์ชัดเจนขึ้น
supercat

1

ขึ้นอยู่กับเนื้อหาของโครงสร้างและคอมไพเลอร์เฉพาะที่ใช้วิธีใดวิธีหนึ่งอาจเร็วกว่า รูปแบบทั่วไปคือโครงสร้างที่ตรงกับเกณฑ์บางอย่างสามารถส่งคืนในรีจิสเตอร์; สำหรับฟังก์ชั่นที่ส่งกลับโครงสร้างประเภทอื่นผู้เรียกจะต้องจัดสรรพื้นที่สำหรับโครงสร้างชั่วคราวที่ใดที่หนึ่ง (โดยทั่วไปจะอยู่บนสแต็ก) และส่งผ่านที่อยู่เป็นพารามิเตอร์ "ซ่อน" ในกรณีที่ฟังก์ชันคืนค่าฟังก์ชันถูกเก็บไว้โดยตรงกับตัวแปรโลคัลซึ่งไม่มีแอดเดรสอยู่โดยโค้ดภายนอกใด ๆ คอมไพเลอร์บางตัวอาจสามารถส่งผ่านแอดเดรสของตัวแปรนั้นได้โดยตรง

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

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


3
จนถึงจุดสุดท้ายของคุณ: ไม่มีสิ่งใดใน C ++ ที่จะหยุดฟังก์ชั่นจากการเก็บสำเนาของตัวชี้ที่ส่งผ่านเป็นข้อมูลอ้างอิงฟังก์ชั่นสามารถใช้ที่อยู่ของวัตถุ นอกจากนี้ยังมีอิสระในการใช้การอ้างอิงเพื่อสร้างวัตถุอื่นที่มีการอ้างอิงนั้น (ไม่ได้สร้างตัวชี้เปล่า) อย่างไรก็ตามการคัดลอกตัวชี้หรือการอ้างอิงในวัตถุอาจอยู่ได้นานกว่าวัตถุที่พวกเขาชี้ไปที่การสร้างตัวชี้ / การอ้างอิงห้อย ดังนั้นจุดเกี่ยวกับความปลอดภัยในการอ้างอิงจึงค่อนข้างเงียบ
cmaster

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

1

อาร์กิวเมนต์หนึ่งที่สนับสนุนรูปแบบ "พารามิเตอร์เอาต์พุต" คืออนุญาตให้ฟังก์ชันส่งคืนรหัสข้อผิดพลาด

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

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


1
errnoแม้ว่าหนึ่งก็สามารถตั้งค่า
Deduplicator

0

ฉันคิดว่าคุณให้ความสำคัญกับการกำหนดค่าเริ่มต้นผ่านพารามิเตอร์เอาท์พุทกับการเริ่มต้นผ่านการคืนสินค้าไม่ใช่ความแตกต่างในการจัดหาข้อโต้แย้งในการก่อสร้าง

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


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