Tại sao chức năng mẫu này không hoạt động như mong đợi?


23

Tôi đã đọc về các hàm mẫu và bị lẫn lộn bởi vấn đề này:

#include <iostream>

void f(int) {
    std::cout << "f(int)\n";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << "  ";
    f(val);
}

void f(double) {
    std::cout << "f(double)\n";
}

template void g<double>(double);

int main() {
    f(1.0); // f(double)
    f(1);   // f(int)
    g(1.0); // d  f(int), this is surprising
    g(1);   // i  f(int)
}

Kết quả là như nhau nếu tôi không viết template void g<double>(double);.

Tôi nghĩ rằng g<double>nên được instantiated sau f(double), và do đó các cuộc gọi đến ftrong gnên gọi f(double). Đáng ngạc nhiên, nó vẫn gọi f(int)trong g<double>. Bất cứ ai có thể giúp tôi hiểu điều này?


Sau khi đọc câu trả lời, tôi nhận ra sự nhầm lẫn của mình thực sự là gì.

Dưới đây là một ví dụ cập nhật. Nó hầu như không thay đổi ngoại trừ việc tôi đã thêm một chuyên ngành cho g<double>:

#include <iostream>

void f(int){cout << "f(int)" << endl;}

template<typename T>
void g(T val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

void f(double){cout << "f(double)" << endl;}

//Now use user specialization to replace
//template void g<double>(double);

template<>
void g<double>(double val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

int main() {
    f(1.0); // f(double)
    f(1);  // f(int)
    g(1.0); // now d  f(double)
    g(1);  // i  f(int)
}

Với chuyên môn hóa người dùng, g(1.0)hành xử như tôi mong đợi.

Trình biên dịch không nên tự động thực hiện việc khởi tạo tương tự này cho g<double>cùng một vị trí (hoặc thậm chí sau đó main(), như được mô tả trong phần 26.3.3 của Ngôn ngữ lập trình C ++ , phiên bản thứ 4)?


3
Cuộc gọi cuối cùng g(1), i f(int)cho tôi. Bạn đã viết d f(double). Đây có phải là một lỗi đánh máy?
HTNW

Đúng. lấy làm tiếc. cập nhật
Zhongqi Cheng

Nguyên tắc cơ bản của mẫu là hỗ trợ sử dụng các thao tác trên các loại người dùng, trong khi vẫn ngăn chặn việc chiếm quyền điều khiển các cuộc gọi thư viện nội bộ bằng các ký hiệu do người dùng khai báo. Đó là một sự thỏa hiệp không thể, vì không có hợp đồng "khái niệm" nào cho các mẫu và đã quá muộn để giới thiệu những "hợp đồng" âm thanh như vậy.
tò mò

Câu trả lời:


12

Tên fnày là một tên phụ thuộc (nó phụ thuộc vào Tthông qua đối số val) và nó sẽ được giải quyết thành hai bước :

  1. Tra cứu không phải ADL kiểm tra các khai báo hàm ... có thể nhìn thấy từ ngữ cảnh định nghĩa mẫu .
  2. ADL kiểm tra các khai báo hàm ... có thể nhìn thấy từ ngữ cảnh định nghĩa mẫu hoặc bối cảnh khởi tạo mẫu .

void f(double)không thể nhìn thấy từ ngữ cảnh định nghĩa mẫu và ADL cũng sẽ không tìm thấy nó, bởi vì

Đối với các đối số của kiểu cơ bản, tập hợp các không gian tên và các lớp liên quan là trống


Chúng tôi có thể sửa đổi một chút ví dụ của bạn:

struct Int {};
struct Double : Int {};

void f(Int) { 
    std::cout << "f(Int)";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << ' ';
    f(val);
    // (f)(val);
}

void f(Double) { 
    std::cout << "f(Double)";
}

int main() {
    g(Double{});
}

Bây giờ ADL sẽ tìm thấy void f(Double)trong bước thứ hai, và đầu ra sẽ là 6Double f(Double). Chúng ta có thể vô hiệu hóa ADL bằng cách viết (f)(val)(hoặc ::f(val)) thay vì f(val). Sau đó, đầu ra sẽ 6Double f(Int), phù hợp với ví dụ của bạn.


Cảm ơn rât nhiều. Tôi đang tự hỏi nơi khởi tạo cho g <double> trong mã. Có phải nó chỉ trước main (). Nếu vậy, không nên định nghĩa g <double> khởi tạo có thể thấy cả f (int) và f (double), và cuối cùng chọn f (double)?
Cheng

@ZhongqiCheng Ở bước 1, chỉ bối cảnh định nghĩa mẫu sẽ được xem xét và từ bối cảnh đó void f(double)không hiển thị - bối cảnh này kết thúc trước khi khai báo. Ở bước 2 ADL sẽ không tìm thấy gì, vì vậy bối cảnh khởi tạo mẫu không đóng vai trò gì ở đây.
Evg

@ZhongqiCheng, trong bản chỉnh sửa của bạn, bạn đã đưa ra một định nghĩa sau void f(double), vì vậy chức năng này được hiển thị từ nó. Bây giờ fkhông phải là một tên phụ thuộc. Nếu có một kết quả phù hợp hơn để f(val);khai báo sau định nghĩa g<double>, nó cũng sẽ không được tìm thấy. Cách duy nhất để "nhìn về phía trước" là ADL (hoặc một số trình biên dịch cũ không thực hiện tra cứu hai pha một cách chính xác).
Evg

Dưới đây là sự hiểu biết của tôi về câu trả lời của bạn. Tôi nên giả sử rằng các mẫu hàm (g <int> và g <double>) được khởi tạo ngay sau định nghĩa mẫu. Vì vậy, nó sẽ không nhìn thấy f (gấp đôi). Điều này có đúng không. Cảm ơn bạn rất nhiều.
Cheng

@ZhongqiCheng, khởi tạo ngay trước đó main(). Họ sẽ không nhìn thấy f(double), bởi vì khi việc khởi tạo xảy ra thì đã quá muộn: giai đoạn một của việc tra cứu đã được thực hiện và nó đã không tìm thấy f(double).
Evg

6

Vấn đề f(double)chưa được tuyên bố tại điểm mà bạn gọi nó; nếu bạn di chuyển khai báo của nó ở phía trước template g, nó sẽ được gọi.

Chỉnh sửa: Tại sao một người sẽ sử dụng khởi tạo thủ công?

(Tôi sẽ chỉ nói về các mẫu hàm, đối số tương tự cũng giữ cho các mẫu lớp.) Công dụng chính là giảm thời gian biên dịch và / hoặc để ẩn mã của mẫu khỏi người dùng.

Chương trình C ++ được tích hợp thành nhị phân theo 2 bước: biên dịch và liên kết. Để biên dịch một lệnh gọi hàm để thành công, chỉ cần tiêu đề của hàm. Để liên kết thành công, một tệp đối tượng chứa phần thân được biên dịch của hàm là cần thiết.

Bây giờ khi trình biên dịch nhìn thấy một cuộc gọi của một hàm templated , những gì nó làm phụ thuộc vào việc nó biết phần thân của khuôn mẫu hay chỉ tiêu đề. Nếu nó chỉ nhìn thấy tiêu đề, nó sẽ làm tương tự như khi hàm không được tạo khuôn mẫu: đặt thông tin về cuộc gọi cho trình liên kết đến tệp đối tượng. Nhưng nếu nó cũng nhìn thấy phần thân của mẫu thì nó cũng thực hiện một điều khác: nó khởi tạo thể hiện đúng của phần thân, biên dịch phần thân này và đặt nó vào tệp đối tượng.

Nếu một số tệp nguồn gọi cùng một thể hiện của hàm templated, thì mỗi tệp đối tượng của chúng sẽ chứa một phiên bản được biên dịch của thể hiện của hàm. (Linker biết về điều này và giải quyết tất cả các cuộc gọi đến một hàm được biên dịch duy nhất, do đó sẽ chỉ có một trong nhị phân cuối cùng của chương trình / thư viện.) Tuy nhiên, để biên dịch từng tệp nguồn, hàm phải được khởi tạo và biên soạn, mất thời gian.

Nó đủ để trình liên kết thực hiện công việc của nó nếu phần thân của hàm nằm trong một tệp đối tượng. Để khởi tạo thủ công mẫu trong tệp nguồn là một cách để trình biên dịch đặt phần thân của hàm vào tệp đối tượng của tệp nguồn được đề cập. (Nó giống như là hàm được gọi, nhưng phần khởi tạo được viết ở nơi gọi hàm không hợp lệ.) Khi thực hiện xong, tất cả các tệp gọi hàm của bạn có thể được biên dịch chỉ biết tiêu đề của hàm, do đó tiết kiệm thời gian cần thiết để khởi tạo và biên dịch phần thân của hàm với mỗi lệnh gọi.

Lý do thứ hai (thực hiện ẩn) có thể có ý nghĩa bây giờ. Nếu một tác giả thư viện muốn người dùng chức năng mẫu của mình có thể sử dụng chức năng đó, cô ấy thường cung cấp cho họ mã của mẫu, để họ có thể tự biên dịch nó. Nếu cô ấy muốn giữ bí mật mã nguồn của mẫu, cô ấy có thể khởi tạo thủ công mẫu trong mã mà cô ấy sử dụng để xây dựng thư viện và cung cấp cho người dùng phiên bản đối tượng thu được thay vì nguồn.

Liệu điều này có ý nghĩa gì?


Tôi sẽ biết ơn nếu bạn có thể giải thích sự khác biệt giữa khởi tạo được trình bày trong mã đầu tiên của tác giả và chuyên môn hóa trong mã thứ hai của tác giả sau khi chỉnh sửa. Tôi đã đọc nhiều lần trang web của cppreference về chuyên môn và khởi tạo và sách, nhưng tôi không hiểu. Cảm ơn bạn
Dev

@Dev: Vui lòng chỉ định câu hỏi của bạn nhiều hơn một chút, tôi không biết phải trả lời gì. Về cơ bản trong trường hợp này, sự khác biệt là khi trình bày chuyên môn hóa trình biên dịch sử dụng nó, trong khi khi không có mặt, trình biên dịch lấy mẫu, tạo một thể hiện của nó và sử dụng thể hiện được tạo này. Trong đoạn mã trên, cả chuyên môn hóa và thể hiện của mẫu đều dẫn đến cùng một mã.
AshleyWilkes

Câu hỏi của tôi chính xác là một phần của mã: "template void g <double> (double);" Nó được đặt tên là khởi tạo trong mẫu lập trình, nếu bạn biết điều đó. Chuyên môn hóa hơi khác một chút, vì nó được khai báo như trong mã thứ hai mà tác giả đã gửi "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "Bạn có thể giải thích cho tôi sự khác biệt?
Dev

@Dev Tôi đã cố gắng để làm điều đó: trình biên dịch sử dụng một chuyên ngành nếu nó có thể; nếu nó không thể thấy sự chuyên môn hóa (ví dụ vì không có), trình biên dịch sẽ tạo một thể hiện của khuôn mẫu và sử dụng thể hiện đó. Trong đoạn mã trên cả mẫu và chuyên môn đều dẫn đến cùng một kết quả, do đó, sự khác biệt duy nhất là ở chỗ trình biên dịch làm gì để có được kết quả đó. Trong các trường hợp khác, chuyên môn hóa có thể chứa bất kỳ triển khai nào, nó không phải có bất kỳ điểm chung nào với mẫu (nhưng đối với tiêu đề phương thức). Rõ ràng hơn?
AshleyWilkes

1
Cái template void g<double>(double);gọi là khởi tạo thủ công (lưu ý rằng templatekhông có dấu ngoặc góc, đó là một tính năng phân biệt của cú pháp); nó báo cho trình biên dịch tạo một thể hiện của phương thức. Ở đây nó có ít tác dụng, nếu nó không có ở đó, trình biên dịch sẽ tạo ra thể hiện tại nơi mà thể hiện được gọi. Tính năng khởi tạo thủ công hiếm khi được sử dụng, tôi sẽ nói lý do tại sao bạn có thể muốn sử dụng nó sau khi bạn xác nhận mọi thứ rõ ràng hơn :-)
AshleyWilkes
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.