2011-09-23 50 views
13

Đây có lẽ là một câu hỏi triết học, nhưng tôi chạy vào các vấn đề sau đây:Tại sao std :: các thể hiện hàm có một hàm tạo mặc định?

Nếu bạn xác định một std :: chức năng, và bạn không khởi tạo nó một cách chính xác, ứng dụng của bạn sẽ sụp đổ, như thế này:

typedef std::function<void(void)> MyFunctionType; 
MyFunctionType myFunction; 
myFunction(); 

Nếu hàm được thông qua như là một cuộc tranh cãi, như thế này:

void DoSomething (MyFunctionType myFunction) 
    { 
    myFunction(); 
    } 

Sau đó, tất nhiên, nó cũng bị treo. Điều này có nghĩa rằng tôi buộc phải thêm kiểm tra mã như thế này:

void DoSomething (MyFunctionType myFunction) 
    { 
    if (!myFunction) return; 
    myFunction(); 
    } 

Yêu cầu các kiểm tra mang lại cho tôi một flash trở lại những ngày C cũ, nơi bạn cũng phải rà soát tất cả đối số con trỏ một cách rõ ràng:

void DoSomething (Car *car, Person *person) 
    { 
    if (!car) return;  // In real applications, this would be an assert of course 
    if (!person) return; // In real applications, this would be an assert of course 
    ... 
    } 

may mắn thay, chúng ta có thể sử dụng tài liệu tham khảo trong C++, mà ngăn cản tôi từ viết những kiểm tra (giả định rằng người gọi đã không vượt qua các nội dung của một nullptr đến chức năng:

void DoSomething (Car &car, Person &person) 
    { 
    // I can assume that car and person are valid 
    } 

Vì vậy, wh y do std :: instance instance có một hàm tạo mặc định? Nếu không có hàm tạo mặc định, bạn sẽ không phải thêm các kiểm tra, giống như các đối số bình thường khác của một hàm. Và trong những trường hợp 'hiếm' mà bạn muốn chuyển một hàm 'std ::' tùy chọn, bạn vẫn có thể truyền con trỏ tới nó (hoặc sử dụng boost :: optional).

+4

Nó không sụp đổ; nó ném một ngoại lệ. –

+0

Đọc về 'functors': http://www.sgi.com/tech/stl/functors.html –

+0

" Tôi buộc phải thêm mã kiểm tra "- đáng tiếc là người gọi của bạn không thể sửa mã của họ thay thế. Có vẻ kỳ quặc để hủy bỏ để cứu họ cần phải xử lý các ngoại lệ. Tuy nhiên, nó có thể tồi tệ hơn, nếu bạn lấy một con trỏ hàm và chúng không bận tâm khởi tạo nó, thì nó sẽ có một giá trị không xác định và hành vi sẽ không được xác định. Có lẽ chúng đang rối tung hơn nữa gọi là 'strlen' hơn là chúng đang gọi chức năng của bạn. –

Trả lời

14

Đúng, nhưng điều này cũng đúng đối với các loại khác. Ví dụ. nếu tôi muốn lớp của tôi có một Person tùy chọn, thì tôi làm cho thành viên dữ liệu của tôi trở thành một Person-pointer. Tại sao không làm tương tự cho std :: functions? Điều gì là đặc biệt về std :: chức năng mà nó có thể có một trạng thái 'không hợp lệ'?

Trạng thái không có trạng thái "không hợp lệ". Đó là không hợp lệ hơn này:

std::vector<int> aVector; 
aVector[0] = 5; 

gì bạn có là một trốngfunction, giống như aVector là một sản phẩm nào vector. Đối tượng ở trạng thái rất được xác định rõ: trạng thái không có dữ liệu.

Bây giờ, chúng ta hãy xem xét "con trỏ đến chức năng" của bạn gợi ý:

void CallbackRegistrar(..., std::function<void()> *pFunc); 

Làm thế nào để bạn phải gọi đó? Vâng, đây là một điều bạn không có thể làm:

void CallbackFunc(); 
CallbackRegistrar(..., CallbackFunc); 

Đó là không được phép vì CallbackFunc là một chức năng, trong khi các loại tham số là một std::function<void()>*. Hai cái đó không chuyển đổi được, do đó trình biên dịch sẽ phàn nàn. Vì vậy, để thực hiện cuộc gọi, bạn phải thực hiện việc này:

void CallbackFunc(); 
CallbackRegistrar(..., new std::function<void()>(CallbackFunc)); 

Bạn vừa mới giới thiệu new vào hình ảnh. Bạn đã phân bổ một tài nguyên; ai sẽ chịu trách nhiệm cho nó? CallbackRegistrar? Rõ ràng, bạn có thể muốn sử dụng một số loại con trỏ thông minh, vì vậy bạn lộn xộn giao diện nhiều hơn với:

void CallbackRegistrar(..., std::shared_ptr<std::function<void()>> pFunc); 

Đó là rất nhiều ít phiền toái API và cruft, chỉ để vượt qua một chức năng xung quanh. Cách đơn giản nhất để tránh điều này là cho phép std::functiontrống. Giống như chúng tôi cho phép std::vector trống. Giống như chúng tôi cho phép std::string trống. Giống như chúng tôi cho phép std::shared_ptr để trống. Và cứ thế.

Để đơn giản là: std::functionchứa một chức năng. Nó là một chủ sở hữu cho một loại có thể gọi. Do đó, có khả năng là nó không chứa loại có thể gọi.

+2

Rõ ràng, bạn muốn 'tăng :: tùy chọn MSalters

+3

@MSalters: Có, điều đó sẽ hoạt động. Nhưng đáng buồn thay 'tùy chọn' không phải là một phần của thư viện chuẩn C++. Và đối với những người nghèo, những linh hồn không may đó không được phép sử dụng Boost, nó đơn giản không phải là một lựa chọn. –

+0

Bình chọn, từ ngữ đẹp mắt: tùy chọn ... nó đơn giản không phải là một lựa chọn, haha! –

5

Một trong những trường hợp sử dụng phổ biến nhất đối với std::function là đăng ký gọi lại, được gọi khi đáp ứng các điều kiện nhất định. Cho phép các cá thể chưa được khởi tạo chỉ có thể đăng ký các cuộc gọi lại khi cần, nếu không bạn sẽ bị buộc phải luôn truyền ít nhất một số chức năng no-op.

+0

Đúng, nhưng điều này cũng đúng đối với các loại khác. Ví dụ. nếu tôi muốn lớp của tôi có một Person tùy chọn, thì tôi làm cho thành viên dữ liệu của tôi trở thành một Person-pointer. Tại sao không làm tương tự cho std :: functions? Điều gì là đặc biệt về std :: chức năng mà nó có thể có một trạng thái 'không hợp lệ'? – Patrick

+4

Theo tôi, bạn sẽ thấy std :: function như một loại con trỏ thông minh cho callables. Bạn sẽ không mong đợi để sử dụng một con trỏ đến một shared_ptr, phải không? –

1

Có những trường hợp bạn không thể khởi tạo mọi thứ khi xây dựng (ví dụ, khi tham số phụ thuộc vào hiệu ứng trên một cấu trúc khác mà lần lượt phụ thuộc vào hiệu ứng đầu tiên ...).

Trong trường hợp này, bạn nhất thiết phải phá vỡ vòng lặp, thừa nhận trạng thái không hợp lệ có thể nhận dạng được sửa sau này. Vì vậy, bạn xây dựng cái đầu tiên là "null", xây dựng phần tử thứ hai và gán lại phần tử đầu tiên. Bạn có thể, thực sự, tránh kiểm tra, nếu ở đâu một hàm được sử dụng - bạn cấp điều đó bên trong hàm tạo của đối tượng nhúng nó, bạn sẽ luôn quay trở lại sau khi gán lại hợp lệ.

+0

Tôi không nghĩ rằng có bất kỳ cách nào để "sửa sau" chức năng trống std ::. Nếu nó trống khi tạo, nó luôn trống rỗng. (Tất nhiên, cho dù bạn coi nó là một trạng thái không hợp lệ hay không, là một vấn đề khác.) – max

+0

@max trông giống như một vấn đề "khái niệm tương tự - khác nhau", với tôi. –

+0

Tôi đoán tôi đã không hiểu đối số của bạn sau đó. Bạn đang nói rằng bạn có thể sử dụng một 'std :: function' trống khi bạn không biết giá trị nào bạn muốn cho đến sau (nghĩa là bạn không thể khởi tạo nó trong ctor). Tôi đồng ý rằng đó sẽ là một trường hợp sử dụng tốt. Những gì tôi không thấy mặc dù, là những gì bạn sẽ làm sau này - khi bạn cuối cùng * làm * biết giá trị mà bạn muốn. Nó không giống như bạn có thể thay đổi một trống 'std :: function' thành bất cứ điều gì khác? – max

5

Câu trả lời có lẽ là lịch sử: std::function có nghĩa là thay thế cho con trỏ hàm và con trỏ hàm có khả năng là NULL. Vì vậy, khi bạn muốn cung cấp khả năng tương thích dễ dàng với các con trỏ hàm, bạn cần cung cấp một trạng thái không hợp lệ.

Trạng thái không hợp lệ có thể nhận dạng là không thực sự cần thiết vì, như bạn đã đề cập, boost::optional thực hiện công việc đó tốt. Vì vậy, tôi muốn nói rằng std::function 's chỉ ở đó vì lợi ích của lịch sử.

9

Thực tế, ứng dụng của bạn sẽ không bị lỗi.

§ 20.8.11.1 Lớp bad_function_call [func.wrap.badcall]

1/ Một ngoại lệ của loại bad_function_call được ném bởi function::operator() (20.8.11.2.4) khi đối tượng chức năng bao bọc không có mục tiêu.

Hành vi được chỉ định hoàn hảo.

+2

Nó có thể được xác định, nhưng đặc điểm kỹ thuật cũng nói rằng chương trình chấm dứt (tức là: treo) trong trường hợp ngoại lệ để lại 'main'. Điều gì sẽ xảy ra nếu bạn không bắt được nó. –

+2

@NicolBolas: Bạn nói đúng, nhưng tôi không thực sự coi đây là một vụ tai nạn vì nhiều thao tác có khả năng gây ra một ngoại lệ: thực tế đơn giản gán một cái gì đó cho 'std :: function' có thể thực hiện cấp phát bộ nhớ. Do đó, không xử lý các ngoại lệ là hoàn toàn không liên quan đến thực tế là 'std :: function' có thể không được khởi tạo với một cuộc gọi lại. –

0

Trong cùng một cách mà bạn có thể thêm một nullstate vào một loại functor mà không có một, bạn có thể quấn một functor với một lớp mà không thừa nhận một nullstate. Cái trước yêu cầu thêm trạng thái, cái sau không yêu cầu trạng thái mới (chỉ là một hạn chế). Vì vậy, trong khi tôi không biết lý do của thiết kế std::function, nó hỗ trợ việc sử dụng có nghĩa là & gọn gàng nhất, bất kể bạn muốn gì.

Cheers & h.,

0

Bạn chỉ cần sử dụng std :: chức năng cho callbacks, bạn có thể sử dụng một hàm template helper đơn giản cho phép gửi các đối số của nó để xử lý nếu nó không phải là rỗng:

template <typename Callback, typename... Ts> 
void SendNotification(const Callback & callback, Ts&&... vs) 
{ 
    if (callback) 
    { 
     callback(std::forward<Ts>(vs)...); 
    } 
} 

Và sử dụng nó theo cách sau:

std::function<void(int, double>> myHandler; 
... 
SendNotification(myHandler, 42, 3.15); 
Các vấn đề liên quan