Khởi tạo nhiều thành viên lớp không đổi bằng một hàm gọi C ++


50

Nếu tôi có hai biến thành viên không đổi khác nhau, cả hai biến cần được khởi tạo dựa trên cùng một lệnh gọi hàm, có cách nào để thực hiện việc này mà không gọi hàm hai lần không?

Ví dụ, một lớp phân số trong đó tử số và mẫu số là không đổi.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

Điều này dẫn đến lãng phí thời gian, vì chức năng GCD được gọi hai lần. Bạn cũng có thể định nghĩa một thành viên lớp mới gcd_a_b, và trước tiên gán đầu ra của gcd cho nó trong danh sách khởi tạo, nhưng sau đó điều này sẽ dẫn đến lãng phí bộ nhớ.

Nói chung, có cách nào để làm điều này mà không lãng phí các cuộc gọi chức năng hoặc bộ nhớ? Có lẽ bạn có thể tạo các biến tạm thời trong một danh sách khởi tạo? Cảm ơn bạn.


5
Bạn có bằng chứng rằng "hàm GCD được gọi hai lần" không? Nó được đề cập hai lần, nhưng đó không giống như trình biên dịch phát ra mã gọi nó hai lần. Một trình biên dịch có thể suy luận rằng đó là một hàm thuần túy và sử dụng lại giá trị của nó ở lần đề cập thứ hai.
Tháp Eric

6
@EricTowers: Có, trình biên dịch đôi khi có thể giải quyết vấn đề trong thực tế đối với một số trường hợp. Nhưng chỉ khi họ có thể thấy định nghĩa (hoặc một số chú thích trong một đối tượng), nếu không thì không có cách nào để chứng minh nó thuần túy. Bạn nên biên dịch với tối ưu hóa thời gian liên kết được kích hoạt, nhưng không phải ai cũng làm được. Và chức năng có thể là trong một thư viện. Hoặc xem xét trường hợp của một chức năng mà không có tác dụng phụ, và gọi đó là đúng một lần là một vấn đề về tính đúng đắn?
Peter Cordes

@EricTowers Điểm thú vị. Tôi đã thực sự cố gắng kiểm tra nó bằng cách đặt một câu lệnh in bên trong hàm GCD, nhưng bây giờ tôi nhận ra rằng điều đó sẽ ngăn nó trở thành một hàm thuần túy.
Qq0

@ Qq0: Bạn có thể kiểm tra bằng cách xem trình biên dịch được tạo asm, ví dụ: sử dụng trình thám hiểm trình biên dịch Godbolt với gcc hoặc clang -O3. Nhưng có lẽ đối với bất kỳ thực hiện kiểm tra đơn giản nào, nó thực sự sẽ thực hiện lệnh gọi hàm. Nếu bạn sử dụng __attribute__((const))hoặc thuần túy trên nguyên mẫu mà không cung cấp định nghĩa có thể nhìn thấy, thì nên để GCC hoặc clang thực hiện loại bỏ biểu hiện phụ chung (CSE) giữa hai cuộc gọi với cùng một đối số. Lưu ý rằng câu trả lời của Drew hoạt động ngay cả đối với các hàm không thuần túy vì vậy nó tốt hơn nhiều và bạn nên sử dụng nó bất cứ khi nào func có thể không nội tuyến.
Peter Cordes

Nói chung, các biến thành viên const không tĩnh là tốt nhất nên tránh. Một trong số ít các lĩnh vực không thể áp dụng mọi thứ. Chẳng hạn, bạn không thể gán các đối tượng lớp. Bạn có thể emplace_back thành một vectơ nhưng chỉ chừng nào giới hạn dung lượng không tăng kích thước.
doug

Câu trả lời:


66

Nói chung, có cách nào để làm điều này mà không lãng phí các cuộc gọi chức năng hoặc bộ nhớ?

Đúng. Điều này có thể được thực hiện với một hàm tạo ủy nhiệm , được giới thiệu trong C ++ 11.

Một constructor ủy nhiệm là một cách rất hiệu quả để có được các giá trị tạm thời cần thiết cho việc xây dựng trước khi bất kỳ biến thành viên nào được khởi tạo.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};

Không quan tâm, liệu chi phí từ việc gọi một nhà xây dựng khác có đáng kể?
Qq0

1
@ Qq0 Bạn có thể quan sát ở đây rằng không có chi phí nào cho phép tối ưu hóa khiêm tốn.
Drew Dormann

2
@ Qq0: C ++ được thiết kế xung quanh các trình biên dịch tối ưu hóa hiện đại. Họ có thể định tuyến một cách tầm thường cho phái đoàn này, đặc biệt nếu bạn làm cho nó hiển thị trong định nghĩa lớp (trong .h), ngay cả khi định nghĩa hàm tạo thực sự không hiển thị cho nội tuyến. tức là gcd()cuộc gọi sẽ nội tuyến vào mỗi lệnh gọi của hàm tạo và chỉ để lại một callhàm tạo riêng 3 toán hạng.
Peter Cordes

10

Các vars thành viên được khởi tạo theo thứ tự chúng được khai báo trong phần giải mã lớp, do đó bạn có thể làm như sau (về mặt toán học)

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

Không cần phải gọi các nhà xây dựng khác hoặc thậm chí làm cho họ.


6
ok hoạt động cho GCD một cách cụ thể, nhưng nhiều trường hợp sử dụng khác có lẽ không thể lấy được const thứ 2 từ các đối số và lần đầu tiên. Và như đã viết, điều này có một phân chia bổ sung, đó là một nhược điểm khác so với lý tưởng mà trình biên dịch có thể không tối ưu hóa. GCD có thể chỉ tốn một bộ phận nên điều này có thể tệ như gọi GCD hai lần. (Giả sử rằng bộ phận chi phối chi phí của các hoạt động khác, giống như nó thường làm trên các CPU hiện đại.)
Peter Cordes

@PeterCordes nhưng giải pháp khác có thêm chức năng gọi và phân bổ thêm bộ nhớ lệnh.
asmmo

1
Bạn đang nói về nhà xây dựng ủy nhiệm của Drew? Điều đó rõ ràng có thể nội tuyến Fraction(a,b,gcd(a,b))phái đoàn vào người gọi, dẫn đến tổng chi phí ít hơn. Trình biên dịch đó dễ thực hiện hơn trình biên dịch hơn là hoàn tác phép chia bổ sung trong phần này. Tôi đã không thử nó trên godbolt.org nhưng bạn có thể nếu bạn tò mò. Sử dụng gcc hoặc clang -O3như một bản dựng thông thường sẽ sử dụng. (C ++ được thiết kế xoay quanh giả định của trình biên dịch tối ưu hóa hiện đại, do đó có các tính năng như constexpr)
Peter Cordes

-3

@Drew Dormann đã đưa ra một giải pháp tương tự như những gì tôi đã nghĩ. Vì OP không bao giờ đề cập đến việc không thể sửa đổi ctor, nên điều này có thể được gọi bằng Fraction f {a, b, gcd(a, b)}:

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

Chỉ có cách này, không có cuộc gọi thứ hai đến một hàm, hàm tạo hay nói cách khác, vì vậy nó không lãng phí thời gian. Và nó không bị lãng phí bộ nhớ vì tạm thời sẽ phải được tạo ra, vì vậy bạn cũng có thể sử dụng nó tốt. Nó cũng tránh một bộ phận phụ.


3
Chỉnh sửa của bạn làm cho nó thậm chí không trả lời câu hỏi. Bây giờ bạn đang yêu cầu người gọi vượt qua đối số thứ 3? Phiên bản gốc của bạn bằng cách sử dụng gán bên trong phần thân của hàm tạo không hoạt động const, nhưng ít nhất hoạt động đối với các loại khác. Và những gì phân chia thêm là bạn "cũng" tránh? Ý bạn là so với câu trả lời của Asmmo?
Peter Cordes

1
Ok, loại bỏ downvote của tôi bây giờ bạn đã giải thích quan điểm của bạn. Nhưng điều này có vẻ khá khủng khiếp và đòi hỏi bạn phải tự sắp xếp một số công cụ xây dựng vào mọi người gọi. Điều này trái ngược với DRY (không lặp lại chính mình) và đóng gói trách nhiệm / nội bộ của lớp. Hầu hết mọi người sẽ không coi đây là một giải pháp chấp nhận được. Cho rằng có một cách C ++ 11 để thực hiện điều này một cách sạch sẽ, không ai nên làm điều này trừ khi có thể họ bị mắc kẹt với phiên bản C ++ cũ hơn và lớp có rất ít cuộc gọi đến hàm tạo này.
Peter Cordes

2
@aconcernedcitizen: Tôi không có ý vì lý do hiệu suất, ý tôi là vì lý do chất lượng mã. Theo cách của bạn, nếu bạn từng thay đổi cách lớp này hoạt động nội bộ, bạn phải đi tìm tất cả các cuộc gọi đến hàm tạo và thay đổi đối số thứ 3 đó. Phần bổ sung đó ,gcd(foo, bar)là phần mã bổ sung có thể và do đó nên được đưa ra khỏi mọi cuộc gọi trong nguồn . Đó là vấn đề về khả năng duy trì / khả năng đọc, không phải hiệu suất. Trình biên dịch rất có thể sẽ nội tuyến nó tại thời gian biên dịch, mà bạn muốn cho hiệu suất.
Peter Cordes

1
@PeterCordes Bạn nói đúng, bây giờ tôi thấy đầu óc mình đã cố định vào giải pháp và tôi đã bỏ qua mọi thứ khác. Dù bằng cách nào, câu trả lời vẫn còn, nếu chỉ để xấu hổ. Bất cứ khi nào tôi có nghi ngờ về nó, tôi sẽ biết nơi để tìm.
một công dân có liên quan

1
Cũng xem xét trường hợp Fraction f( x+y, a+b ); Để viết theo cách của bạn, bạn sẽ phải viết BadFraction f( x+y, a+b, gcd(x+y, a+b) );hoặc sử dụng tmp vars. Hoặc thậm chí tệ hơn, nếu bạn muốn viết Fraction f( foo(x), bar(y) );- thì bạn cần trang web gọi để khai báo một số vmp để giữ giá trị trả về hoặc gọi lại các hàm đó và hy vọng trình biên dịch CSE sẽ loại bỏ chúng, đó là điều chúng tôi đang tránh. Bạn có muốn gỡ lỗi trường hợp một người gọi trộn lẫn các đối số gcdđể nó không thực sự là GCD của 2 đối số đầu tiên được truyền cho hàm tạo không? Không? Sau đó, đừng biến lỗi đó thành có thể.
Peter Cordes
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.