Việc chuyển đổi một phương thức C ++ thành một hàm C với một đối số con trỏ là một mẫu có thể chấp nhận?


16

Tôi sử dụng C ++ trên ESP-32. Khi đăng ký hẹn giờ tôi phải làm điều này:

timer_args.callback = reinterpret_cast<esp_timer_cb_t>(&SoundMixer::soundCallback);
timer_args.arg = this;

Ở đây hẹn giờ gọi soundCallback.

Và điều tương tự khi đăng ký một nhiệm vụ:

xTaskCreate(reinterpret_cast<TaskFunction_t>(&SoundProviderTask::taskProviderCode), "SProvTask", stackSize, this, 10, &taskHandle);

Vì vậy, phương thức được bắt đầu trong một nhiệm vụ riêng biệt.

GCC luôn cảnh báo tôi về những chuyển đổi này, nhưng nó hoạt động đúng như kế hoạch.

Có được chấp nhận trong mã sản xuất? Có cách nào tốt hơn để làm điều này?

Câu trả lời:


47

A reinterpret_castluôn luôn tanh trừ khi bạn biết chính xác những gì bạn đang làm. Ở đây, mã của bạn chỉ hoạt động do quy ước gọi của GCC cho các phương thức C ++, nhưng điều này có mùi giống như hành vi không xác định. Cụ thể, bạn không nên cho rằng các hàm thành viên tương thích với các con trỏ hàm thông thường.

Cách tiếp cận thông thường sẽ là thay vào đó xác định hàm tương thích C với chữ ký thích hợp, bên trong gọi phương thức C ++. Ví dụ:

extern "C" static void my_timer_callback(void* arg) {
  static_cast<SoundMixer*>(arg)->soundCallback();
}

Dàn diễn viên này vẫn ổn vì chúng ta đang chuyển từ một void*sang loại đối tượng nhọn.

Chi tiết:

  • extern "C"chỉ định liên kết ngôn ngữ của chức năng này. Liên kết ngôn ngữ ảnh hưởng đến việc xáo trộn tên và quy ước gọi hàm. Các chức năng thành viên không thể có liên kết ngôn ngữ C. Liên kết ngôn ngữ phần lớn là trực giao với liên kết bên trong / bên ngoài.

  • Đối với một cuộc gọi lại, chức năng có thể là riêng tư, tức là có liên kết nội bộ. Mã C không bao giờ đề cập đến cuộc gọi lại theo tên. Đoạn mã trên chỉ định liên kết nội bộ thông qua statictừ khóa (không phải là phương thức tĩnh!). Ngoài ra, chức năng có thể đã được đặt vào một không gian tên ẩn danh.

    Tôi không hoàn toàn chắc chắn về các tương tác giữa extern "C"static(liên kết nội bộ). Ví dụ như [dcl.link]nói rằng “Tất cả các loại chức năng, tên hàm với mối liên hệ bên ngoài, và tên biến với mối liên hệ bên ngoài có một mối liên hệ ngôn ngữ.” Tôi giải thích điều này để các loại của my_timer_callbackcó mối liên hệ ngôn ngữ C, nhưng mà chức năng của nó tên gì không.

  • A static_castthích hợp ở đây vì chúng ta biết loại thực của argnhưng không thể biểu thị nó trong hệ thống loại. Ngược lại, a reinterpret_castlà thích hợp khi chúng ta muốn diễn giải lại một mẫu bit, ví dụ như một con trỏ tới một kiểu số.

  • Các hàm không phải là đối tượng bình thường và các hàm thành viên thậm chí còn ít hơn như vậy. Bạn có thể diễn giải lại giữa các loại con trỏ hàm miễn là hàm chỉ được gọi thông qua kiểu thực của nó (và tương tự cho các con trỏ hàm thành viên). Cho dù bạn có thể truyền con trỏ hàm cho các loại khác (ví dụ: con trỏ đối tượng hoặc con trỏ void) được xác định theo thực hiện ( nền ). Trên phôi POSIX giữa các con trỏ hàm và void*được phép để dlsym()có thể hoạt động. Các phôi khác liên quan đến con trỏ hàm (thành viên) không được xác định. Cụ thể, phôi giữa các hàm thành viên và con trỏ hàm là không thể.


1
Không phải std::bindcũng giả sử con trỏ đối tượng là đối số phương thức đầu tiên?
val nói Phục hồi lại

5
@val Có, nhưng điều đó không có nghĩa là các hàm thành viên tương thích với các hàm thông thường, chỉ đơn thuần là bind () sử dụng thuật toán INVOKE xử lý các hàm thành viên như một trường hợp riêng biệt với các đối tượng hàm thông thường. con trỏ hàm. Vì std :: bind () tạo ra một functor, nó không phù hợp để giao tiếp với C.
amon

1
Một câu hỏi khác: tại sao tôi cần extern "C"ở đây? Là liên kết C quan trọng trong trường hợp này?
val nói Phục hồi lại

5
@val Nếu bạn muốn có thể gọi hàm đó từ C, nó phải sử dụng quy ước gọi C. Điều này có thể được thực hiện bằng cách khai báo hàm đó bằng liên kết ngôn ngữ C hoặc bằng các phần mở rộng dành riêng cho trình biên dịch (như __attribute__((cdecl)), nhưng vui lòng không làm điều đó). Hàm C ++ không được đảm bảo có quy ước gọi tương thích C khác (mặc dù trong GCC, nó thường hoạt động tốt).
amon

4
@val Để biết chi tiết về lý do tại sao extern "C"chính thức cần thiết, hãy xem [dcl.link]"Hai loại chức năng có liên kết ngôn ngữ khác nhau là các loại khác nhau ngay cả khi chúng giống hệt nhau." và [expr.call]"Gọi một chức năng thông qua một biểu thức có loại chức năng khác với loại chức năng của chức năng được gọi là kết quả trong hành vi chưa hoàn thành"
Ben Voigt

-1

Cá nhân, cách tiếp cận tương thích, dễ thực hiện và dễ hiểu nhất mà tôi đã tìm thấy là chỉ cung cấp chức năng "trình bao bọc", tương thích với giao diện C dự kiến, gọi bên trong phương thức (và trong trường hợp không phải là tĩnh, khởi tạo hoặc sử dụng một thể hiện hiện có để làm điều đó). Nó có thể được coi là một dạng biến thể của Mẫu thiết kế bộ điều hợp.


6
Đó không phải là những gì amon trả lời sao?
Dronz

1
@Dronz sau một lần đọc thứ hai, vâng, chủ yếu là vậy. Ngay khi đọc statictôi đã thấy nó là một phương thức và vì một số lý do không nhận ra nó không vượt qua thiscon trỏ như là đối số đầu tiên (và cuộc tranh luận sau đây về việc sử dụng std::bindnó được củng cố). Nhưng vâng, bạn hoàn toàn đúng! (Xin lỗi vì câu trả lời kép!)
Jesus Alonso Abad

3
Vâng, staticcó ít nhất ba ý nghĩa khác nhau. Và bạn sẽ trộn chúng lên nếu bạn không cẩn thận. Tôi có thể nói, thật sự hữu ích để hiểu sự khác biệt giữa các cách sử dụng khác nhau static, vì mỗi thứ là một công cụ tuyệt vời theo đúng nghĩa của nó.
cmaster - phục hồi monica
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.