Tôi nên khởi tạo cấu trúc C thông qua tham số, hoặc bằng giá trị trả về? [đóng cửa]


33

Công ty tôi làm việc đang khởi tạo tất cả các cấu trúc dữ liệu của họ thông qua chức năng khởi tạo như vậy:

//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);

Tôi đã bị đẩy lùi khi cố gắng khởi tạo các cấu trúc của mình như thế này:

//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);

Có một lợi thế cho người này hơn người kia?
Tôi nên làm cái nào, và làm thế nào để chứng minh nó là một cách thực hành tốt hơn?


4
@gnat Đây là một câu hỏi rõ ràng về khởi tạo cấu trúc. Chủ đề đó thể hiện một số lý do tương tự mà tôi muốn thấy được áp dụng cho quyết định thiết kế cụ thể này.
Trevor Hickey

2
@Jefffrey Chúng tôi ở C, vì vậy chúng tôi thực sự không thể có phương pháp. Nó không phải luôn luôn là một bộ giá trị trực tiếp. Đôi khi, khởi tạo một cấu trúc là để có được các giá trị (bằng cách nào đó) và thực hiện một số logic để khởi tạo cấu trúc.
Trevor Hickey

1
@JacquesB Tôi nhận được "Mọi thành phần mà bạn xây dựng sẽ khác với các thành phần khác. Có một hàm Khởi tạo () được sử dụng ở nơi khác cho cấu trúc. Về mặt kỹ thuật, gọi nó là hàm tạo là sai."
Trevor Hickey

1
@TrevorHickey InitializeFoo()là một nhà xây dựng. Sự khác biệt duy nhất từ ​​một hàm tạo C ++ là, thiscon trỏ được truyền một cách rõ ràng chứ không phải ngầm định. Mã được biên dịch InitializeFoo()và một C ++ tương ứng Foo::Foo()hoàn toàn giống nhau.
cmaster 20/07/2015

2
Tùy chọn tốt hơn: Ngừng sử dụng C trên C ++. Tự động.
Thomas Eding 21/07/2015

Câu trả lời:


25

Trong cách tiếp cận thứ 2, bạn sẽ không bao giờ có một Foo khởi tạo một nửa. Đặt tất cả các công trình ở một nơi có vẻ hợp lý và rõ ràng hơn.

Nhưng ... cách thứ nhất không tệ lắm và thường được sử dụng trong nhiều lĩnh vực (thậm chí còn có một cuộc thảo luận về cách tốt nhất để tiêm phụ thuộc, hoặc tiêm tài sản như cách thứ nhất của bạn, hoặc tiêm xây dựng như cách thứ 2) . Không phải là sai.

Vì vậy, nếu không có gì sai và phần còn lại của công ty sử dụng cách tiếp cận số 1, thì bạn nên phù hợp với cơ sở mã hiện có và không cố gắng làm rối nó bằng cách giới thiệu một mẫu mới. Đây thực sự là yếu tố quan trọng nhất khi chơi ở đây, chơi đẹp với những người bạn mới của bạn và đừng cố gắng trở thành bông tuyết đặc biệt, người làm những điều khác biệt.


Ok, có vẻ hợp lý. Tôi có ấn tượng rằng việc khởi tạo một đối tượng mà không thể thấy loại đầu vào nào đang khởi tạo nó, sẽ dẫn đến nhầm lẫn. Tôi đã cố gắng làm theo khái niệm dữ liệu trong / dữ liệu ra để tạo ra mã có thể dự đoán và kiểm tra được. Làm theo cách khác dường như đã tăng khả năng ghép nối vì tệp nguồn của cấu trúc của tôi cần phụ thuộc thêm để thực hiện khởi tạo. Mặc dù vậy, bạn đã đúng, ở chỗ tôi không muốn chèo thuyền trừ khi một cách được ưu tiên hơn so với cách khác.
Trevor Hickey

4
@TrevorHickey: Thật ra tôi sẽ nói có hai điểm khác biệt chính giữa các ví dụ bạn đưa ra - (1) Trong một chức năng được truyền một con trỏ đến cấu trúc để khởi tạo, và trong khi đó, nó trả về một cấu trúc được khởi tạo; (2) Trong một tham số khởi tạo được truyền vào hàm và trong các tham số khác chúng được ẩn. Bạn dường như đang hỏi thêm về (2), nhưng câu trả lời ở đây đang tập trung vào (1). Bạn có thể muốn làm rõ điều đó - Tôi nghi ngờ hầu hết mọi người sẽ khuyên bạn nên kết hợp cả hai bằng cách sử dụng tham số rõ ràng và một con trỏ:void SetupFoo(Foo *out, int a, int b, int c)
psmears 20/07/2015

1
Cách tiếp cận đầu tiên sẽ dẫn đến một Foocấu trúc "nửa khởi tạo" như thế nào? Cách tiếp cận đầu tiên cũng thực hiện tất cả các khởi tạo ở một nơi. (Hoặc bạn đang xem một cấu trúc chưa được khởi tạo Foolà "nửa khởi tạo"?)
jamesdlin 21/07/2015

1
@jamesdlin trong trường hợp Foo được tạo và Khởi tạo vô tình bị bỏ lỡ. Nó chỉ là một con số của bài phát biểu để mô tả khởi tạo 2 pha mà không cần gõ một mô tả dài. Tôi hình dung loại nhà phát triển có kinh nghiệm mọi người sẽ hiểu.
gbjbaanb

22

Cả hai cách tiếp cận bó mã khởi tạo vào một lệnh gọi hàm duy nhất. Càng xa càng tốt.

Tuy nhiên, có hai vấn đề với cách tiếp cận thứ hai:

  1. Cái thứ hai không thực sự xây dựng đối tượng kết quả, nó khởi tạo một đối tượng khác trên ngăn xếp, sau đó được sao chép sang đối tượng cuối cùng. Đây là lý do tại sao tôi sẽ thấy cách tiếp cận thứ hai là hơi kém. Việc đẩy lùi mà bạn đã nhận được có thể là do bản sao không liên quan này.

    Điều này thậm chí còn tệ hơn khi bạn lấy được một lớp Derivedtừ Foo(các cấu trúc được sử dụng chủ yếu cho hướng đối tượng trong C): Với cách tiếp cận thứ hai, hàm ConstructDerived()sẽ gọi ConstructFoo(), sao chép Foođối tượng tạm thời kết quả vào khe siêu lớp của một Derivedđối tượng; kết thúc việc khởi tạo Derivedđối tượng; chỉ để có đối tượng kết quả được sao chép lại khi trở về. Thêm một lớp thứ ba, và toàn bộ điều trở nên hoàn toàn vô lý.

  2. Với cách tiếp cận thứ hai, các ConstructClass()chức năng không có quyền truy cập vào địa chỉ của đối tượng đang được xây dựng. Điều này làm cho không thể liên kết các đối tượng trong quá trình xây dựng, vì nó cần thiết khi một đối tượng cần đăng ký chính nó với một đối tượng khác để gọi lại.


Cuối cùng, không phải tất cả structsđều là lớp học chính thức. Một số structshiệu quả chỉ là bó một loạt các biến với nhau, mà không có bất kỳ hạn chế nội bộ nào đối với các giá trị của các biến này. typedef struct Point { int x, y; } Point;sẽ là một ví dụ tốt về điều này. Đối với việc sử dụng một chức năng khởi tạo có vẻ quá mức cần thiết. Trong những trường hợp này, cú pháp chữ ghép có thể thuận tiện (đó là C99):

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

hoặc là

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

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

5
Tôi không nghĩ các bản sao sẽ là một vấn đề vì en.wikipedia.org/wiki/Copy_elision
Trevor Hickey

5
Việc trình biên dịch có thể bỏ qua bản sao không làm giảm bớt sự thật rằng bạn đã viết ra bản sao. Trong C, viết các hoạt động không cần thiết và dựa vào trình biên dịch để sửa chúng được coi là lỗi xấu. Điều này khác với C ++, nơi mọi người tự hào khi họ có thể chứng minh rằng trình biên dịch về mặt lý thuyết có thể loại bỏ tất cả các hành trình còn lại bởi các mẫu lồng nhau của họ. Trong C, mọi người cố gắng viết chính xác mã mà máy nên thực thi. Dù sao, quan điểm về các địa chỉ không thể truy cập vẫn còn, sao chép bản sao có thể giúp bạn ở đó.
cmaster

3
Bất cứ ai sử dụng trình biên dịch nên mong đợi mã họ viết sẽ được chuyển đổi bởi trình biên dịch. Trừ khi họ đang chạy trình thông dịch C phần cứng, mã họ viết sẽ không phải là mã họ thực thi, ngay cả khi có thể dễ dàng tin vào điều khác. Nếu họ hiểu trình biên dịch của họ, họ sẽ hiểu elision và không khác gì int x = 3;không lưu trữ chuỗi xtrong nhị phân. Địa chỉ và điểm thừa kế là tốt; sự thất bại giả định của cuộc bầu cử là ngu ngốc.
Yakk 21/07/2015

@Yakk: Trong lịch sử, C được phát minh để phục vụ như một dạng ngôn ngữ lắp ráp cấp cao cho lập trình hệ thống. Trong những năm kể từ đó, danh tính của nó ngày càng trở nên âm u. Một số người muốn nó là ngôn ngữ ứng dụng được tối ưu hóa, nhưng vì không có hình thức ngôn ngữ lắp ráp cấp cao nào xuất hiện, C vẫn cần thiết để phục vụ vai trò sau này. Tôi thấy không có gì sai với ý tưởng rằng mã chương trình được viết tốt nên hành xử ít nhất là ngay cả khi được biên dịch với tối ưu hóa tối thiểu, mặc dù để làm cho nó thực sự hoạt động sẽ yêu cầu C thêm một số thứ mà nó đã thiếu từ lâu.
supercat

@Yakk: Chẳng hạn, có các lệnh sẽ cho trình biên dịch "Các biến sau có thể được lưu giữ an toàn trong các thanh ghi trong đoạn mã sau" cũng như một phương thức sao chép khối một loại khác ngoài việc unsigned charcho phép tối ưu hóa Quy tắc bí danh nghiêm ngặt sẽ không đủ, đồng thời làm cho kỳ vọng của lập trình viên rõ ràng hơn.
supercat

1

Tùy thuộc vào nội dung của cấu trúc và trình biên dịch cụ thể đang được sử dụng, một trong hai cách tiếp cận có thể nhanh hơn. Một mô hình điển hình là các cấu trúc đáp ứng các tiêu chí nhất định có thể được trả về trong các thanh ghi; đối với các hàm trả về các kiểu cấu trúc khác, người gọi được yêu cầu phân bổ không gian cho cấu trúc tạm thời ở đâu đó (thường là trên ngăn xếp) và truyền địa chỉ của nó dưới dạng tham số "ẩn"; trong trường hợp trả về của hàm được lưu trữ trực tiếp vào một biến cục bộ có địa chỉ không được giữ bởi bất kỳ mã bên ngoài nào, một số trình biên dịch có thể truyền trực tiếp địa chỉ của biến đó.

Nếu một loại cấu trúc đáp ứng các yêu cầu của việc triển khai cụ thể được trả về trong các thanh ghi (ví dụ: không lớn hơn một từ máy hoặc điền chính xác hai từ máy) có chức năng trả về cấu trúc có thể nhanh hơn việc chuyển địa chỉ của cấu trúc, đặc biệt là kể từ khi để lộ địa chỉ của một biến ra bên ngoài mã có thể giữ một bản sao của nó có thể ngăn cản một số tối ưu hóa hữu ích. Nếu một loại không thỏa mãn các yêu cầu như vậy, mã được tạo cho một hàm trả về một cấu trúc sẽ tương tự như loại cho một hàm chấp nhận một con trỏ đích; mã gọi có thể sẽ nhanh hơn đối với biểu mẫu lấy con trỏ, nhưng biểu mẫu đó sẽ mất một số cơ hội tối ưu hóa.

Thật tệ khi C không cung cấp một phương tiện để nói rằng một hàm bị cấm giữ một bản sao của một con trỏ được truyền vào (ngữ nghĩa tương tự như một tham chiếu C ++) vì việc truyền một con trỏ bị hạn chế như vậy sẽ có được lợi thế về hiệu suất trực tiếp khi vượt qua một con trỏ tới một đối tượng tồn tại từ trước, nhưng đồng thời tránh các chi phí ngữ nghĩa của việc yêu cầu trình biên dịch xem xét địa chỉ của một biến "bị lộ".


3
Đến điểm cuối cùng của bạn: Không có gì trong C ++ để ngăn chặn một chức năng giữ một bản sao của một con trỏ được truyền vào làm tham chiếu, hàm có thể chỉ cần lấy địa chỉ của đối tượng. Cũng có thể sử dụng tham chiếu để xây dựng một đối tượng khác có chứa tham chiếu đó (không tạo con trỏ trần). Tuy nhiên, con trỏ sao chép hoặc tham chiếu trong đối tượng có thể tồn tại lâu hơn đối tượng mà chúng trỏ tới, tạo ra một con trỏ / tham chiếu lơ lửng. Vì vậy, quan điểm về an toàn tham chiếu là khá câm.
cmaster

@cmaster: Trên các nền tảng trả về cấu trúc bằng cách chuyển con trỏ đến bộ lưu trữ tạm thời, trình biên dịch C không cung cấp các hàm được gọi với bất kỳ cách truy cập địa chỉ của bộ lưu trữ đó. Trong C ++, có thể lấy địa chỉ của một biến được truyền bằng tham chiếu, nhưng trừ khi người gọi đảm bảo tuổi thọ của vật phẩm được truyền (trong trường hợp đó thường sẽ chuyển qua một con trỏ) Hành vi không xác định sẽ có kết quả.
supercat

1

Một đối số có lợi cho kiểu "tham số đầu ra" là nó cho phép hàm trả về mã lỗi.

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!
}

Xem xét một số tập hợp các cấu trúc liên quan, nếu việc khởi tạo có thể thất bại đối với bất kỳ trong số chúng, có thể đáng để tất cả chúng sử dụng kiểu tham số ngoài cho mục đích nhất quán.


1
Mặc dù người ta chỉ có thể thiết lập errno.
Ded repeatator

0

Tôi cho rằng trọng tâm của bạn là khởi tạo thông qua đầu ra so với khởi tạo thông qua trả về, không phải là sự khác biệt trong cách cung cấp đối số xây dựng.

Lưu ý rằng cách tiếp cận đầu tiên có thể cho phép Foomờ đục (mặc dù không phải với cách bạn hiện đang sử dụng nó) và đó thường là mong muốn cho khả năng duy trì lâu dài. Bạn có thể xem xét, ví dụ, một hàm phân bổ một Foocấu trúc mờ mà không khởi tạo nó. Hoặc có lẽ bạn cần khởi tạo lại một Foocấu trúc đã được khởi tạo trước đó với các giá trị khác nhau.


Downvoter, quan tâm để giải thích? Là một cái gì đó tôi nói thực tế không chính xác?
jamesdlin 21/07/2015
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.