Câu trả lời được chấp nhận cho câu hỏi này về sự hướng nội của chức năng thành viên, mặc dù nó rất phổ biến, có một nhược điểm có thể được quan sát trong chương trình sau:
#include <type_traits>
#include <iostream>
#include <memory>
/* Here we apply the accepted answer's technique to probe for the
the existence of `E T::operator*() const`
*/
template<typename T, typename E>
struct has_const_reference_op
{
template<typename U, E (U::*)() const> struct SFINAE {};
template<typename U> static char Test(SFINAE<U, &U::operator*>*);
template<typename U> static int Test(...);
static const bool value = sizeof(Test<T>(0)) == sizeof(char);
};
using namespace std;
/* Here we test the `std::` smart pointer templates, including the
deprecated `auto_ptr<T>`, to determine in each case whether
T = (the template instantiated for `int`) provides
`int & T::operator*() const` - which all of them in fact do.
*/
int main(void)
{
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value << endl;
return 0;
}
Được xây dựng với GCC 4.6.3, nghiệm thu sản phẩm 110
- thông báo cho chúng ta biết rằng
T = std::shared_ptr<int>
không không cung cấp int & T::operator*() const
.
Nếu bạn chưa khôn ngoan với gotcha này, thì hãy xem định nghĩa của
std::shared_ptr<T>
tiêu đề <memory>
sẽ làm sáng tỏ. Trong triển khai đó, std::shared_ptr<T>
được bắt nguồn từ một lớp cơ sở mà nó kế thừa operator*() const
. Vì vậy, việc khởi tạo mẫu tạo
SFINAE<U, &U::operator*>
thành "tìm kiếm" toán tử
U = std::shared_ptr<T>
sẽ không xảy ra, bởi vì std::shared_ptr<T>
không có
operator*()
quyền riêng của nó và khởi tạo mẫu không "thực hiện kế thừa".
Snag này không ảnh hưởng đến phương pháp SFINAE nổi tiếng, sử dụng "Thủ thuật sizeof ()", để phát hiện chỉ đơn thuần T
có một số chức năng thành viên mf
(xem ví dụ
câu trả lời và nhận xét này). Nhưng thiết lập T::mf
tồn tại thường là (thường?) Không đủ tốt: bạn cũng có thể cần phải thiết lập rằng nó có chữ ký mong muốn. Đó là nơi điểm số kỹ thuật minh họa. Biến thể con trỏ của chữ ký mong muốn được ghi trong một tham số của loại mẫu phải được thỏa mãn bằng cách
&T::mf
thăm dò SFINAE để thành công. Nhưng kỹ thuật khởi tạo mẫu này đưa ra câu trả lời sai khi T::mf
được kế thừa.
Một kỹ thuật SFINAE an toàn cho việc xem xét nội bộ T::mf
phải tránh sử dụng &T::mf
trong một đối số khuôn mẫu để khởi tạo một kiểu mà độ phân giải mẫu hàm SFINAE phụ thuộc vào. Thay vào đó, độ phân giải chức năng mẫu SFINAE chỉ có thể phụ thuộc vào các khai báo loại thích hợp chính xác được sử dụng làm loại đối số của hàm thăm dò SFINAE bị quá tải.
Bằng cách trả lời cho câu hỏi tuân theo ràng buộc này, tôi sẽ minh họa cho việc phát hiện đồng thời E T::operator*() const
, tùy ý T
và E
. Mô hình tương tự sẽ áp dụng mutatis mutandis
để thăm dò cho bất kỳ chữ ký phương thức thành viên nào khác.
#include <type_traits>
/*! The template `has_const_reference_op<T,E>` exports a
boolean constant `value that is true iff `T` provides
`E T::operator*() const`
*/
template< typename T, typename E>
struct has_const_reference_op
{
/* SFINAE operator-has-correct-sig :) */
template<typename A>
static std::true_type test(E (A::*)() const) {
return std::true_type();
}
/* SFINAE operator-exists :) */
template <typename A>
static decltype(test(&A::operator*))
test(decltype(&A::operator*),void *) {
/* Operator exists. What about sig? */
typedef decltype(test(&A::operator*)) return_type;
return return_type();
}
/* SFINAE game over :( */
template<typename A>
static std::false_type test(...) {
return std::false_type();
}
/* This will be either `std::true_type` or `std::false_type` */
typedef decltype(test<T>(0,0)) type;
static const bool value = type::value; /* Which is it? */
};
Trong giải pháp này, chức năng thăm dò SFINAE bị quá tải test()
được "gọi đệ quy". (Tất nhiên nó hoàn toàn không được gọi; nó chỉ có các kiểu trả về giả thuyết được giải quyết bởi trình biên dịch.)
Chúng ta cần thăm dò ít nhất một và nhiều nhất hai điểm thông tin:
- Có
T::operator*()
tồn tại không? Nếu không, chúng ta đã xong.
- Cho rằng
T::operator*()
tồn tại, là chữ ký của nó
E T::operator*() const
?
Chúng tôi nhận được câu trả lời bằng cách đánh giá loại trả về của một cuộc gọi đến test(0,0)
. Điều đó được thực hiện bởi:
typedef decltype(test<T>(0,0)) type;
Cuộc gọi này có thể được giải quyết khi /* SFINAE operator-exists :) */
quá tải test()
hoặc có thể giải quyết /* SFINAE game over :( */
tình trạng quá tải. Nó không thể giải quyết /* SFINAE operator-has-correct-sig :) */
tình trạng quá tải, bởi vì người ta chỉ mong đợi một đối số và chúng ta sẽ vượt qua hai đối số.
Tại sao chúng ta đi qua hai? Đơn giản chỉ để buộc nghị quyết loại trừ
/* SFINAE operator-has-correct-sig :) */
. Đối số thứ hai không có ý nghĩa khác.
Lệnh gọi test(0,0)
này sẽ giải quyết /* SFINAE operator-exists :) */
chỉ trong trường hợp đối số đầu tiên 0 làm bão hòa loại tham số đầu tiên của tình trạng quá tải đó, đó là decltype(&A::operator*)
, với A = T
. 0 sẽ đáp ứng loại đó chỉ trong trường hợp T::operator*
tồn tại.
Giả sử trình biên dịch nói "Có với điều đó. Sau đó, nó sẽ đi
/* SFINAE operator-exists :) */
và nó cần xác định kiểu trả về của lệnh gọi hàm, trong trường hợp đó là decltype(test(&A::operator*))
- kiểu trả về của một cuộc gọi khác test()
.
Lần này, chúng ta sẽ vượt qua chỉ một đối số &A::operator*
, mà bây giờ chúng ta biết là tồn tại, hoặc chúng ta sẽ không ở đây. Một cuộc gọi đến test(&A::operator*)
có thể giải quyết một /* SFINAE operator-has-correct-sig :) */
hoặc một lần nữa để có thể giải quyết /* SFINAE game over :( */
. Cuộc gọi sẽ khớp
/* SFINAE operator-has-correct-sig :) */
chỉ trong trường hợp &A::operator*
thỏa mãn loại tham số duy nhất của tình trạng quá tải đó, đó là E (A::*)() const
, với A = T
.
Trình biên dịch sẽ nói Có ở đây nếu T::operator*
có chữ ký mong muốn đó, và sau đó lại phải đánh giá kiểu trả về của quá tải. Không còn "thu hồi" bây giờ: nó là std::true_type
.
Nếu trình biên dịch không chọn /* SFINAE operator-exists :) */
cho cuộc gọi test(0,0)
hoặc không chọn /* SFINAE operator-has-correct-sig :) */
cho cuộc gọi test(&A::operator*)
, thì trong cả hai trường hợp nó sẽ đi cùng
/* SFINAE game over :( */
và kiểu trả về cuối cùng là std::false_type
.
Dưới đây là một chương trình thử nghiệm cho thấy mẫu tạo ra các câu trả lời dự kiến trong các trường hợp mẫu khác nhau (GCC 4.6.3 một lần nữa).
// To test
struct empty{};
// To test
struct int_ref
{
int & operator*() const {
return *_pint;
}
int & foo() const {
return *_pint;
}
int * _pint;
};
// To test
struct sub_int_ref : int_ref{};
// To test
template<typename E>
struct ee_ref
{
E & operator*() {
return *_pe;
}
E & foo() const {
return *_pe;
}
E * _pe;
};
// To test
struct sub_ee_ref : ee_ref<char>{};
using namespace std;
#include <iostream>
#include <memory>
#include <vector>
int main(void)
{
cout << "Expect Yes" << endl;
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value;
cout << has_const_reference_op<std::vector<int>::iterator,int &>::value;
cout << has_const_reference_op<std::vector<int>::const_iterator,
int const &>::value;
cout << has_const_reference_op<int_ref,int &>::value;
cout << has_const_reference_op<sub_int_ref,int &>::value << endl;
cout << "Expect No" << endl;
cout << has_const_reference_op<int *,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,char &>::value;
cout << has_const_reference_op<unique_ptr<int>,int const &>::value;
cout << has_const_reference_op<unique_ptr<int>,int>::value;
cout << has_const_reference_op<unique_ptr<long>,int &>::value;
cout << has_const_reference_op<int,int>::value;
cout << has_const_reference_op<std::vector<int>,int &>::value;
cout << has_const_reference_op<ee_ref<int>,int &>::value;
cout << has_const_reference_op<sub_ee_ref,int &>::value;
cout << has_const_reference_op<empty,int &>::value << endl;
return 0;
}
Có những sai sót mới trong ý tưởng này? Nó có thể được làm cho chung chung hơn mà không một lần nữa phạm lỗi của snag nó tránh?