Tại sao lại sử dụng một tay cầm mờ đục mà yêu cầu đúc trong API công khai chứ không phải là con trỏ cấu trúc an toàn kiểu?


27

Tôi đang đánh giá một thư viện có API công khai hiện tại trông như thế này:

libengine.h

/* Handle, used for all APIs */
typedef size_t enh;


/* Create new engine instance; result returned in handle */
int en_open(int mode, enh *handle);

/* Start an engine */
int en_start(enh handle);

/* Add a new hook to the engine; hook handle returned in h2 */
int en_add_hook(enh handle, int hooknum, enh *h2);

Lưu ý rằng đó enhlà một tay cầm chung, được sử dụng như một tay cầm cho một số kiểu dữ liệu khác nhau ( động cơmóc ).

Trong nội bộ, hầu hết các API này dĩ nhiên chuyển "xử lý" sang cấu trúc bên trong mà chúng đã malloc:

động cơ.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, *enh handle)
{
    struct engine *en;

    en = malloc(sizeof(*en));
    if (!en)
        return -1;

    // ...initialization...

    *handle = (enh)en;
    return 0;
}

int en_start(enh handle)
{
    struct engine *en = (struct engine*)handle;

    return en->start(en);
}

Cá nhân, tôi ghét giấu những thứ đằng sau typedefs, đặc biệt là khi nó ảnh hưởng đến sự an toàn kiểu. (Đưa ra một enh, làm thế nào để tôi biết những gì nó thực sự đề cập đến?)

Vì vậy, tôi đã gửi yêu cầu kéo, đề xuất thay đổi API sau (sau khi sửa đổi toàn bộ thư viện để tuân thủ):

libengine.h

struct engine;           /* Forward declaration */
typedef size_t hook_h;    /* Still a handle, for other reasons */


/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);

/* Start an engine */
int en_start(struct engine *en);

/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);

Tất nhiên, điều này làm cho việc triển khai API nội bộ trông tốt hơn rất nhiều, loại bỏ các phôi và duy trì an toàn loại đến / từ quan điểm của người tiêu dùng.

libengine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, struct engine **en)
{
    struct engine *_e;

    _e = malloc(sizeof(*_e));
    if (!_e)
        return -1;

    // ...initialization...

    *en = _e;
    return 0;
}

int en_start(struct engine *en)
{
    return en->start(en);
}

Tôi thích điều này vì những lý do sau:

Tuy nhiên, chủ sở hữu của dự án đã bỏ qua yêu cầu kéo (diễn giải):

Cá nhân tôi không thích ý tưởng phơi bày struct engine. Tôi vẫn nghĩ rằng cách hiện tại là sạch sẽ và thân thiện hơn.

Ban đầu tôi sử dụng một kiểu dữ liệu khác cho tay cầm hook, nhưng sau đó quyết định chuyển sang sử dụng enh, vì vậy tất cả các loại tay cầm đều chia sẻ cùng một kiểu dữ liệu để giữ cho nó đơn giản. Nếu điều này gây nhầm lẫn, chúng tôi chắc chắn có thể sử dụng một loại dữ liệu khác.

Hãy xem những gì người khác nghĩ về PR này.

Thư viện này hiện đang ở giai đoạn beta riêng tư, vì vậy không có nhiều mã người tiêu dùng phải lo lắng (chưa). Ngoài ra, tôi đã xáo trộn tên một chút.


Làm thế nào là một tay cầm mờ tốt hơn so với một cấu trúc mờ có tên?

Lưu ý: Tôi đã hỏi câu hỏi này tại Code Review , nơi nó đã bị đóng.


1
Tôi đã chỉnh sửa tiêu đề thành một cái gì đó mà tôi tin rằng thể hiện rõ ràng hơn cốt lõi của câu hỏi của bạn. Vui lòng hoàn nguyên nếu tôi giải thích sai.
Ixrec

1
@Ixrec Điều đó tốt hơn, cảm ơn bạn. Sau khi viết lên toàn bộ câu hỏi, tôi hết năng lực tinh thần để đưa ra một tiêu đề hay.
Jonathon Reinhart

Câu trả lời:


33

Câu thần chú "đơn giản là tốt hơn" đã trở nên quá giáo điều. Đơn giản không phải lúc nào cũng tốt hơn nếu nó làm phức tạp những thứ khác. Hội là đơn giản - mỗi lệnh đơn giản hơn nhiều so với các lệnh ngôn ngữ cấp cao hơn - và các chương trình hội đồng phức tạp hơn các ngôn ngữ cấp cao hơn làm cùng một việc. Trong trường hợp của bạn, loại tay cầm thống nhất enhlàm cho các loại đơn giản hơn với chi phí làm cho các chức năng trở nên phức tạp. Vì các loại dự án thường có xu hướng tăng theo tỷ lệ tuyến tính so với các hàm của nó, vì dự án trở nên lớn hơn, bạn thường thích các loại phức tạp hơn nếu nó có thể làm cho các hàm đơn giản hơn - vì vậy về mặt này, cách tiếp cận của bạn có vẻ đúng.

Tác giả của dự án lo ngại rằng cách tiếp cận của bạn là " phơi bàystruct engine ". Tôi đã có thể giải thích với họ rằng nó không phơi bày cấu trúc - chỉ có một thực tế là có một cấu trúc được đặt tên engine. Người dùng của thư viện đã cần phải biết về loại đó - ví dụ, họ cần biết rằng đối số đầu tiên en_add_hooklà loại đó và đối số đầu tiên thuộc loại khác. Vì vậy, nó thực sự làm cho API phức tạp hơn, vì thay vì có tài liệu "chữ ký" của hàm, các loại này cần phải được ghi lại ở một nơi khác và vì trình biên dịch không còn có thể xác minh các loại cho lập trình viên.

Một điều cần lưu ý - API mới của bạn làm cho mã người dùng phức tạp hơn một chút, thay vì viết:

enh en;
en_open(ENGINE_MODE_1, &en);

Bây giờ họ cần cú pháp phức tạp hơn để khai báo xử lý của họ:

struct engine* en;
en_open(ENGINE_MODE_1, &en);

Tuy nhiên, giải pháp khá đơn giản:

struct _engine;
typedef struct _engine* engine

và bây giờ bạn có thể viết thẳng:

engine en;
en_open(ENGINE_MODE_1, &en);

Tôi quên đề cập rằng thư viện tuyên bố tuân theo Phong cách mã hóa Linux , điều này cũng xảy ra với những gì tôi làm theo. Ở đó, bạn sẽ thấy rằng các cấu trúc đánh máy chỉ để tránh viết structbị làm nản lòng.
Jonathon Reinhart

@JonathonReinhart anh ấy đã gõ con trỏ để cấu trúc chứ không phải cấu trúc.
ratchet freak

@JonathonReinhart và thực sự đọc liên kết đó tôi thấy rằng đối với "các đối tượng hoàn toàn mờ đục" thì được phép. (chương 5 quy tắc a)
ratchet freak

Có, nhưng chỉ trong những trường hợp đặc biệt hiếm. Tôi thành thật tin rằng đã được thêm vào để tránh viết lại tất cả các mã mm để đối phó với các typedefs pte. Nhìn vào mã khóa spin. Nó là hoàn toàn cụ thể vòm (không có dữ liệu chung) nhưng họ không bao giờ sử dụng một typedef.
Jonathon Reinhart

8
Tôi thích typedef struct engine engine;và sử dụng engine*: Một tên ít hơn được giới thiệu, và điều đó làm cho nó rõ ràng là một tay cầm như thế FILE*.
Ded repeatator

16

Dường như có một sự nhầm lẫn ở cả hai phía ở đây:

  • sử dụng phương pháp xử lý không yêu cầu sử dụng một loại tay cầm cho tất cả các tay cầm
  • phơi bày structtên không tiết lộ chi tiết của nó (chỉ tồn tại của nó)

Có những lợi thế khi sử dụng tay cầm thay vì con trỏ trần, trong ngôn ngữ như C, bởi vì việc bàn giao con trỏ cho phép thao tác trực tiếp với con trỏ (bao gồm cả lệnh gọi free) trong khi bàn giao tay cầm yêu cầu khách hàng phải thông qua API để thực hiện bất kỳ hành động nào .

Tuy nhiên, cách tiếp cận để có một loại tay cầm duy nhất, được xác định thông qua một typedefloại không an toàn và có thể gây ra nhiều đau buồn.

Do đó, đề nghị cá nhân của tôi sẽ là hướng tới loại tay cầm an toàn, mà tôi nghĩ sẽ làm hài lòng cả hai bạn. Điều này được thực hiện khá đơn giản:

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

Bây giờ, người ta không thể vô tình vượt qua 2như một tay cầm và cũng không thể vô tình chuyển một tay cầm cho một cây chổi nơi mà một tay cầm cho động cơ được mong đợi.


Vì vậy, tôi đã gửi yêu cầu kéo, đề xuất thay đổi API sau (sau khi sửa đổi toàn bộ thư viện để tuân thủ)

Đó là sai lầm của bạn: trước khi tham gia vào công việc quan trọng trên thư viện nguồn mở, hãy liên hệ với (các) tác giả / người bảo trì để thảo luận về sự thay đổi trước . Nó sẽ cho phép cả hai bạn đồng ý về những việc cần làm (hoặc không làm), và tránh những công việc không cần thiết và sự thất vọng xảy ra từ đó.


1
Cảm ơn bạn. Bạn đã không đi vào làm gì với tay cầm mặc dù. Tôi đã triển khai API dựa trên xử lý thực tế , trong đó con trỏ không bao giờ bị lộ, ngay cả khi thông qua một typedef. Nó liên quan đến việc ~ tra cứu dữ liệu đắt tiền ở đầu vào của mọi lệnh gọi API - giống như cách Linux tìm kiếm struct filetừ một int fd. Điều này chắc chắn là quá mức cần thiết cho một thư viện chế độ người dùng IMO.
Jonathon Reinhart

@JonathonReinhart: Chà, vì thư viện đã cung cấp tay cầm, tôi không cảm thấy cần phải mở rộng. Thật vậy, có nhiều cách tiếp cận, từ việc đơn giản chuyển đổi con trỏ sang số nguyên sang có một "nhóm" và sử dụng ID làm khóa. Bạn thậm chí có thể chuyển đổi cách tiếp cận giữa Gỡ lỗi (ID + tra cứu, để xác thực) và Phát hành (chỉ con trỏ được chuyển đổi, cho tốc độ).
Matthieu M.

Việc sử dụng lại chỉ mục bảng số nguyên sẽ thực sự gặp phải vấn đề ABA , trong đó một đối tượng (chỉ mục 3) được giải phóng, sau đó một đối tượng mới được tạo và không may bị gán 3lại chỉ mục . Nói một cách đơn giản, khó có cơ chế trọn đời đối tượng an toàn trong C trừ khi tính tham chiếu (cùng với các quy ước về quyền sở hữu chung đối với các đối tượng) được đưa vào một phần rõ ràng của thiết kế API.
rwong

2
@rwong: Đó chỉ là một vấn đề trong sơ đồ ngây thơ; bạn có thể dễ dàng tích hợp một bộ đếm epoch, ví dụ, để khi một tay cầm cũ được chỉ định, bạn sẽ nhận được một khớp nối epoch.
Matthieu M.

1
Đề xuất @JonathonReinhart: bạn có thể đề cập đến "quy tắc răng cưa nghiêm ngặt" trong câu hỏi của bạn để giúp điều khiển cuộc thảo luận về các khía cạnh quan trọng hơn.
rwong

3

Đây là một tình huống mà cần xử lý mờ đục;

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

Khi thư viện có hai hoặc nhiều kiểu cấu trúc có cùng một phần tiêu đề của các trường, như "kiểu" ở trên, các kiểu cấu trúc này có thể được coi là có cấu trúc cha chung (như một lớp cơ sở trong C ++).

Bạn có thể định nghĩa phần tiêu đề là "struct engine", như thế này;

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

Nhưng đó là một quyết định tùy chọn vì cần loại phôi bất kể sử dụng công cụ struct.

Phần kết luận

Trong một số trường hợp, có những lý do tại sao tay cầm mờ được sử dụng thay vì các cấu trúc mờ có tên.


Tôi nghĩ rằng việc sử dụng một liên minh làm cho điều này an toàn hơn thay vì các diễn viên nguy hiểm đến các lĩnh vực có thể được di chuyển. Kiểm tra ý chính này tôi đưa ra một ví dụ hoàn chỉnh.
Jonathon Reinhart

Nhưng trên thực tế, việc tránh ngay switchtừ đầu, bằng cách sử dụng "các chức năng ảo" có lẽ là lý tưởng và giải quyết toàn bộ vấn đề.
Jonathon Reinhart

Thiết kế của bạn tại ý chính phức tạp hơn tôi đề nghị. Chắc chắn, nó làm cho việc truyền ít hơn, loại an toàn và thông minh, nhưng giới thiệu nhiều mã và loại hơn. Theo tôi, nó dường như trở nên quá phức tạp để có được loại an toàn. Tôi, và có lẽ tác giả thư viện quyết định theo dõi KISS thay vì an toàn kiểu.
Akio Takahashi

Chà, nếu bạn muốn giữ cho nó thực sự đơn giản, bạn hoàn toàn có thể bỏ qua việc kiểm tra lỗi!
Jonathon Reinhart

Theo tôi, sự đơn giản trong thiết kế được ưa thích hơn một số lượng kiểm tra lỗi. Trong trường hợp này, việc kiểm tra lỗi như vậy chỉ tồn tại trong các hàm API. Hơn nữa, bạn có thể loại bỏ các kiểu phôi bằng cách sử dụng union, nhưng hãy nhớ rằng union là loại không an toàn.
Akio Takahashi

2

Lợi ích rõ ràng nhất của phương pháp xử lý là bạn có thể sửa đổi các cấu trúc bên trong mà không phá vỡ API bên ngoài. Cấp, bạn vẫn phải sửa đổi phần mềm máy khách, nhưng ít nhất bạn không thay đổi giao diện.

Một điều khác là nó cung cấp khả năng chọn từ nhiều loại khác nhau có thể có trong thời gian chạy, mà không phải cung cấp giao diện API rõ ràng cho từng loại. Một số ứng dụng, như đọc cảm biến từ một số loại cảm biến khác nhau trong đó mỗi cảm biến hơi khác nhau và tạo ra dữ liệu hơi khác nhau, đáp ứng tốt với phương pháp này.

Vì dù sao bạn cũng sẽ cung cấp các cấu trúc cho khách hàng của mình, nên bạn hy sinh một chút an toàn loại (vẫn có thể được kiểm tra trong thời gian chạy) cho API đơn giản hơn nhiều, mặc dù cần phải truyền.


5
"Bạn có thể sửa đổi các cấu trúc bên trong mà không cần .." - bạn cũng có thể với phương pháp khai báo chuyển tiếp.
dùng253751

Không phải cách tiếp cận "khai báo chuyển tiếp" vẫn yêu cầu bạn khai báo chữ ký loại sao? Và những chữ ký kiểu đó vẫn không thay đổi nếu bạn thay đổi cấu trúc?
Robert Harvey

Khai báo chuyển tiếp chỉ yêu cầu bạn khai báo tên của loại - cấu trúc của nó vẫn bị ẩn.
Idan Arye

Vậy thì lợi ích của việc khai báo chuyển tiếp là gì nếu nó thậm chí không thực thi cấu trúc kiểu?
Robert Harvey

6
@RobertHarvey Hãy nhớ rằng - đây là C mà chúng ta đang nói đến. Không có phương thức, vì vậy ngoài tên và cấu trúc, không có gì khác cho kiểu. Nếu nó đã thực thi cấu trúc nó đã có được giống hệt nhau để khai thường xuyên. Điểm phơi bày tên mà không thực thi cấu trúc là bạn có thể sử dụng loại đó trong chữ ký hàm. Tất nhiên, không có cấu trúc, bạn chỉ có thể sử dụng các con trỏ cho loại, vì trình biên dịch không thể biết kích thước của nó, nhưng vì không có con trỏ ẩn trong C sử dụng con trỏ là đủ tốt để gõ tĩnh để bảo vệ bạn.
Idan Arye

2

Dêjà vu

Làm thế nào là một tay cầm mờ tốt hơn so với một cấu trúc mờ có tên?

Tôi đã gặp chính xác cùng một kịch bản, chỉ với một số khác biệt tinh tế. Chúng tôi đã có, trong SDK của chúng tôi, rất nhiều thứ như thế này:

typedef void* SomeHandle;

Đề xuất đơn thuần của tôi là làm cho nó phù hợp với các loại nội bộ của chúng tôi:

typedef struct SomeVertex* SomeHandle;

Đối với các bên thứ ba sử dụng SDK, nó sẽ không có gì khác biệt. Đó là một loại mờ đục. Ai quan tâm? Nó không ảnh hưởng đến ABI * hoặc khả năng tương thích nguồn và sử dụng các bản phát hành mới của SDK yêu cầu plugin phải được biên dịch lại bằng mọi cách.

* Lưu ý rằng, như gnasher chỉ ra, thực sự có thể có trường hợp kích thước của một cái gì đó như con trỏ tới struct và void * thực sự có thể là một kích thước khác trong trường hợp nó sẽ ảnh hưởng đến ABI. Giống như anh ấy, tôi chưa bao giờ gặp nó trong thực tế. Nhưng từ quan điểm đó, thứ hai thực sự có thể cải thiện tính di động trong một số bối cảnh tối nghĩa, vì vậy đó là một lý do khác để ủng hộ thứ hai, mặc dù có lẽ là tranh luận cho hầu hết mọi người.

Lỗi bên thứ ba

Hơn nữa, tôi thậm chí còn có nhiều lý do hơn là loại an toàn để phát triển / gỡ lỗi nội bộ. Chúng tôi đã có một số nhà phát triển plugin gặp lỗi trong mã của họ vì hai tay cầm tương tự ( PanelPanelNew, tức là) đều sử dụng void*typedef cho tay cầm của họ và họ đã vô tình chuyển nhầm tay cầm sang vị trí sai do chỉ sử dụng void*cho tất cả. Vì vậy, nó thực sự đã gây ra lỗi cho những người sử dụngSDK. Các lỗi của họ cũng khiến nhóm phát triển nội bộ tốn rất nhiều thời gian, vì họ sẽ gửi báo cáo lỗi phàn nàn về các lỗi trong SDK của chúng tôi và chúng tôi phải gỡ lỗi plugin và thấy rằng đó thực sự là do lỗi trong plugin chuyển qua xử lý sai đến những vị trí sai (có thể dễ dàng cho phép mà không cần cảnh báo khi mọi tay cầm là bí danh cho void*hoặc size_t). Vì vậy, chúng tôi đã lãng phí thời gian của mình để cung cấp dịch vụ sửa lỗi cho bên thứ ba vì những lỗi gây ra từ phía họ bởi mong muốn của chúng tôi về sự thuần khiết trong khái niệm trong việc che giấu tất cả thông tin nội bộ, thậm chí chỉ là tên của nội bộ của chúng tôi structs.

Giữ Typedef

Sự khác biệt là tôi đã đề xuất rằng chúng tôi typedefvẫn giữ nguyên, không để khách hàng viết struct SomeVertex, điều này sẽ ảnh hưởng đến khả năng tương thích nguồn cho các bản phát hành plugin trong tương lai. Mặc dù cá nhân tôi thích ý tưởng không đánh máy structtrong C, từ góc độ SDK, điều này typedefcó thể giúp vì toàn bộ vấn đề là độ mờ đục. Vì vậy, tôi khuyên bạn nên thư giãn tiêu chuẩn này chỉ dành cho API được công khai. Đối với các máy khách sử dụng SDK, không quan trọng việc một tay cầm là con trỏ tới cấu trúc, số nguyên, v.v. Điều duy nhất quan trọng với chúng là hai tay cầm khác nhau không bí danh cùng loại dữ liệu để chúng không không chính xác vượt qua trong xử lý sai đến sai địa điểm.

Loại thông tin

Trường hợp quan trọng nhất để tránh truyền là với bạn, các nhà phát triển nội bộ. Kiểu thẩm mỹ này để che giấu tất cả các tên nội bộ khỏi SDK là một tính thẩm mỹ khái niệm nào đó có chi phí đáng kể là mất tất cả thông tin loại và yêu cầu chúng tôi không cần phải sử dụng các trình gỡ lỗi để lấy thông tin quan trọng. Trong khi một lập trình viên C phần lớn nên quen với điều này trong C, để yêu cầu điều này một cách không cần thiết chỉ là yêu cầu sự cố.

Ý tưởng khái niệm

Nói chung, bạn muốn coi chừng những loại nhà phát triển đã đưa ra một số ý tưởng khái niệm về độ tinh khiết vượt xa tất cả các nhu cầu thực tế, hàng ngày. Những thứ đó sẽ thúc đẩy khả năng duy trì của codebase của bạn xuống đất trong việc tìm kiếm một lý tưởng không tưởng nào đó, khiến cả đội tránh kem dưỡng da trong sa mạc vì sợ rằng nó không tự nhiên và có thể gây thiếu hụt vitamin D trong khi một nửa phi hành đoàn đang chết vì ung thư da.

Ưu tiên người dùng

Ngay cả từ quan điểm nghiêm ngặt về người dùng của những người sử dụng API, họ sẽ thích API lỗi hơn hay API hoạt động tốt nhưng để lộ một số tên mà họ khó có thể quan tâm khi trao đổi? Bởi vì đó là sự đánh đổi thực tế. Mất thông tin loại không cần thiết bên ngoài bối cảnh chung sẽ làm tăng nguy cơ lỗi và từ một cơ sở mã hóa quy mô lớn trong môi trường toàn đội trong một số năm, luật Murphy có xu hướng được áp dụng khá nhiều. Nếu bạn tăng nguy cơ lỗi một cách không cần thiết, rất có thể bạn sẽ ít nhất gặp thêm một số lỗi. Không mất quá nhiều thời gian trong một nhóm lớn để thấy rằng mọi loại sai lầm của con người có thể tưởng tượng được cuối cùng sẽ đi từ một tiềm năng thành hiện thực.

Vì vậy, có lẽ đó là một câu hỏi đặt ra cho người dùng. "Bạn có thích SDK buggier hoặc một cái lộ ra một số tên mờ đục nội bộ mà bạn thậm chí không bao giờ quan tâm?" Và nếu câu hỏi đó dường như đưa ra một sự phân đôi giả, tôi muốn nói rằng cần có nhiều kinh nghiệm trong toàn đội trong một môi trường quy mô rất lớn để đánh giá cao thực tế rằng nguy cơ cao hơn cho các lỗi sẽ xuất hiện các lỗi thực sự trong dài hạn. Điều quan trọng là nhà phát triển tự tin đến mức nào để tránh lỗi. Trong cài đặt toàn nhóm, sẽ giúp suy nghĩ nhiều hơn về các liên kết yếu nhất và ít nhất là cách dễ nhất và nhanh nhất để ngăn chặn chúng vấp ngã.

Đề nghị

Vì vậy, tôi đề nghị một sự thỏa hiệp ở đây vẫn sẽ cung cấp cho bạn khả năng giữ lại tất cả các lợi ích gỡ lỗi:

typedef struct engine* enh;

... thậm chí với cái giá là đánh máy đi struct, điều đó có thực sự giết chúng ta không? Có lẽ là không, vì vậy tôi cũng khuyên bạn nên thực hiện một số chủ nghĩa thực dụng, nhưng hơn nữa đối với nhà phát triển muốn gỡ lỗi theo cấp số nhân bằng cách sử dụng size_tở đây và chuyển sang / từ số nguyên không có lý do chính đáng nào ngoài việc ẩn thêm thông tin đã 99 % ẩn cho người dùng và không thể gây hại nhiều hơn size_t.


1
Đó là một sự khác biệt nhỏ : Theo Tiêu chuẩn C, tất cả "con trỏ tới cấu trúc" có biểu diễn giống hệt nhau, do đó, tất cả "con trỏ tới liên kết", do đó, "void *" và "char *", nhưng một khoảng trống * và "con trỏ" to struct "có thể có sizeof khác nhau () và / hoặc đại diện khác nhau. Trong thực tế, tôi chưa bao giờ thấy điều này.
gnasher729

@ gnasher729 Tương tự, có lẽ tôi nên đủ điều kiện cho phần đó liên quan đến khả năng mất tính di động khi truyền tới void*hoặc size_tquay lại như một lý do khác để tránh bỏ qua một cách thừa thãi. Tôi đã bỏ qua nó vì thực tế tôi chưa bao giờ thấy nó trong thực tế với các nền tảng mà chúng tôi nhắm mục tiêu (luôn là các nền tảng máy tính để bàn: linux, OSX, Windows).

1
Chúng tôi đã kết thúc vớitypedef struct uc_struct uc_engine;
Jonathon Reinhart

1

Tôi nghi ngờ lý do thực sự là quán tính, đó là những gì họ đã luôn làm và nó hoạt động vậy tại sao lại thay đổi nó?

Lý do chính tôi có thể thấy là tay cầm mờ cho phép nhà thiết kế đặt bất cứ thứ gì đằng sau nó, không chỉ là một cấu trúc. Nếu API trả về và chấp nhận nhiều loại mờ, tất cả chúng đều trông giống với người gọi và không bao giờ có bất kỳ vấn đề biên dịch hoặc biên dịch lại nào nếu bản in đẹp thay đổi. Nếu en_NewFlidgetTwiddler (xử lý ** newTwiddler) thay đổi để trả về một con trỏ cho Twiddler thay vì một điều khiển, API sẽ không thay đổi và bất kỳ mã mới nào sẽ âm thầm sử dụng một con trỏ trước khi sử dụng tay cầm. Đồng thời, không có mối nguy hiểm nào đối với HĐH hoặc bất cứ điều gì khác lặng lẽ "sửa" con trỏ nếu nó vượt qua các ranh giới.

Tất nhiên, nhược điểm của điều đó là người gọi có thể cho bất cứ thứ gì vào đó. Bạn có một điều 64 bit? Đưa nó vào khe 64 bit trong lệnh gọi API và xem điều gì sẽ xảy ra.

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

Cả hai biên dịch nhưng tôi đặt cược chỉ một trong số họ làm những gì bạn muốn.


1

Tôi tin rằng thái độ bắt nguồn từ triết lý lâu đời để bảo vệ API thư viện C khỏi sự lạm dụng của người mới bắt đầu.

Đặc biệt,

  • Các tác giả thư viện biết đó là một con trỏ tới cấu trúc và các chi tiết của cấu trúc được hiển thị với mã thư viện.
  • Tất cả các lập trình viên có kinh nghiệm sử dụng thư viện cũng biết đó là một con trỏ đến một số cấu trúc mờ đục;
    • Họ đã có đủ kinh nghiệm đau đớn để biết không làm phiền với các byte được lưu trữ trong các cấu trúc đó.
  • Lập trình viên thiếu kinh nghiệm cũng không biết.
    • Họ sẽ cố gắng để memcpydữ liệu mờ hoặc tăng các byte hoặc từ bên trong cấu trúc. Đi hack.

Các biện pháp đối phó truyền thống lâu dài là:

  • Giả mạo thực tế rằng một tay cầm mờ thực sự là một con trỏ đến một cấu trúc mờ tồn tại trong cùng một không gian quá trình-bộ nhớ.
    • Để làm như vậy bằng cách tuyên bố nó là một giá trị nguyên có cùng số bit như một void*
    • Để có thêm chu vi, giả trang các bit của con trỏ, vd
      struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);

Điều này chỉ để nói rằng nó có truyền thống lâu đời; Tôi không có ý kiến ​​cá nhân về việc nó đúng hay sai.


3
Ngoại trừ xor nực cười, giải pháp của tôi cũng cung cấp sự an toàn đó. Khách hàng vẫn không biết về kích thước hoặc nội dung của cấu trúc, với lợi ích bổ sung về an toàn kiểu. Tôi không thấy việc sử dụng size_t để giữ con trỏ là tốt hơn thế nào.
Jonathon Reinhart

@JonathonReinhart điều cực kỳ khó xảy ra là khách hàng sẽ thực sự không biết về cấu trúc. Câu hỏi là nhiều hơn: họ có thể có được cấu trúc và họ có thể trả lại một phiên bản sửa đổi cho thư viện của bạn không. Không chỉ với nguồn mở, mà nói chung hơn. Giải pháp là phân vùng bộ nhớ hiện đại, không phải XOR ngớ ngẩn.
Móż

Bạn đang nói về cái gì vậy? Tất cả những gì tôi nói là bạn không thể biên dịch bất kỳ mã nào cố gắng hủy bỏ con trỏ thành cấu trúc đã nói hoặc làm bất cứ điều gì đòi hỏi kiến ​​thức về kích thước của nó. Chắc chắn, bạn có thể ghi nhớ (, 0,) trong toàn bộ quá trình, nếu bạn thực sự muốn.
Jonathon Reinhart

6
Lập luận này nghe có vẻ giống như bảo vệ chống lại Machiavelli . Nếu người dùng muốn chuyển rác vào API của tôi, không có cách nào tôi có thể ngăn chặn chúng. Việc giới thiệu một giao diện không an toàn kiểu như thế này hầu như không giúp ích gì cho việc đó, vì nó thực sự làm cho việc lạm dụng API vô tình dễ dàng hơn.
ComicSansMS

@ComicSansMS cảm ơn bạn đã đề cập đến "tình cờ", vì đó là điều tôi thực sự đang cố gắng ngăn chặn ở đây.
Jonathon Reinhart
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.