Làm thế nào để có thể trả lại một đối tượng, trong C ++?


167

Tôi biết tiêu đề nghe có vẻ quen thuộc vì có nhiều câu hỏi tương tự, nhưng tôi đang hỏi về một khía cạnh khác của vấn đề (tôi biết sự khác biệt giữa việc có mọi thứ trên chồng và đặt chúng vào đống).

Trong Java tôi luôn có thể trả về các tham chiếu đến các đối tượng "cục bộ"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

Trong C ++, để làm một cái gì đó tương tự tôi có 2 tùy chọn

(1) Tôi có thể sử dụng tài liệu tham khảo bất cứ khi nào tôi cần "trả lại" một đối tượng

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Sau đó sử dụng nó như thế này

Thing thing;
calculateThing(thing);

(2) Hoặc tôi có thể trả về một con trỏ tới một đối tượng được phân bổ động

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Sau đó sử dụng nó như thế này

Thing* thing = calculateThing();
delete thing;

Sử dụng cách tiếp cận đầu tiên tôi sẽ không phải giải phóng bộ nhớ theo cách thủ công, nhưng với tôi nó làm cho mã khó đọc. Vấn đề với cách tiếp cận thứ hai là, tôi sẽ phải nhớ delete thing;, trông không đẹp lắm. Tôi không muốn trả về một giá trị được sao chép vì nó không hiệu quả (tôi nghĩ vậy), vì vậy đây là câu hỏi

  • Có giải pháp thứ ba (không yêu cầu sao chép giá trị) không?
  • Có bất kỳ vấn đề nếu tôi dính vào giải pháp đầu tiên?
  • Khi nào và tại sao tôi nên sử dụng giải pháp thứ hai?

32
+1 để đặt câu hỏi độc đáo.
Kangkan

1
Nói một cách rất khoa trương, thật thiếu chính xác khi nói rằng "các hàm trả về một cái gì đó". Chính xác hơn, việc đánh giá một lời gọi hàm tạo ra một giá trị . Giá trị luôn là một đối tượng (trừ khi đó là hàm void). Sự khác biệt là liệu giá trị là giá trị glvalue hay prvalue - được xác định bởi loại trả về khai báo có phải là tham chiếu hay không.
Kerrek SB

Câu trả lời:


107

Tôi không muốn trả về giá trị sao chép vì nó không hiệu quả

Chứng minh điều đó.

Tra cứu RVO và NRVO, và trong ngữ nghĩa di chuyển C ++ 0x. Trong hầu hết các trường hợp trong C ++ 03, tham số out chỉ là một cách tốt để làm cho mã của bạn trở nên xấu xí, và trong C ++ 0x, bạn thực sự đang tự làm tổn thương mình bằng cách sử dụng tham số out.

Chỉ cần viết mã sạch, trả về theo giá trị. Nếu hiệu suất là một vấn đề, hãy lập hồ sơ (ngừng đoán) và tìm những gì bạn có thể làm để khắc phục nó. Nó có khả năng sẽ không trả lại mọi thứ từ các chức năng.


Điều đó nói rằng, nếu bạn đã chết khi viết như vậy, có lẽ bạn muốn thực hiện tham số out. Nó tránh phân bổ bộ nhớ động, an toàn hơn và thường nhanh hơn. Nó đòi hỏi bạn phải có một số cách để xây dựng đối tượng trước khi gọi hàm, điều này không phải lúc nào cũng có ý nghĩa đối với tất cả các đối tượng.

Nếu bạn muốn sử dụng phân bổ động, ít nhất có thể được thực hiện là đặt nó trong một con trỏ thông minh. (Dù sao thì việc này cũng nên được thực hiện mọi lúc)


10
@phunehehe: Không có điểm nào là đầu cơ, bạn nên lập hồ sơ mã của mình và tìm hiểu. (Gợi ý: không.) Trình biên dịch rất thông minh, họ sẽ không lãng phí thời gian để sao chép mọi thứ xung quanh nếu họ không phải làm vậy. Ngay cả khi sao chép chi phí một cái gì đó, bạn vẫn nên cố gắng để có mã tốt hơn mã nhanh; mã tốt là dễ dàng để tối ưu hóa khi tốc độ trở thành một vấn đề. Không có điểm nào trong việc làm xấu mã lên cho một cái gì đó bạn không có ý tưởng là một vấn đề; đặc biệt là nếu bạn thực sự làm nó chậm lại hoặc không nhận được gì từ nó. Và nếu bạn đang sử dụng C ++ 0x, di chuyển ngữ nghĩa làm cho điều này không thành vấn đề.
GManNickG

1
@GMan, re: RVO: thực ra điều này chỉ đúng nếu người gọi và callee của bạn tình cờ ở cùng một đơn vị biên dịch, trong thế giới thực, nó không phải là hầu hết thời gian. Vì vậy, bạn sẽ thất vọng nếu mã của bạn không được tạo khuôn mẫu (trong trường hợp đó tất cả sẽ nằm trong một đơn vị biên dịch) hoặc bạn có một số tối ưu hóa thời gian liên kết tại chỗ (GCC chỉ có mã từ 4,5).
Alex B

2
@Alex: Trình biên dịch ngày càng tốt hơn trong việc tối ưu hóa giữa các đơn vị dịch thuật. (VC thực hiện nó cho một số bản phát hành ngay bây giờ.)
sbi

9
@Alex B: Đây là rác hoàn chỉnh. Nhiều quy ước gọi điện rất phổ biến làm cho người gọi chịu trách nhiệm phân bổ không gian cho các giá trị trả lại lớn và callee chịu trách nhiệm cho việc xây dựng của họ. RVO vui vẻ làm việc trên các đơn vị biên dịch ngay cả khi không tối ưu hóa thời gian liên kết.
CB Bailey

6
@Charles, khi kiểm tra nó có vẻ đúng! Tôi rút lại tuyên bố sai lệch rõ ràng của tôi.
Alex B

41

Chỉ cần tạo đối tượng và trả lại nó

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Tôi nghĩ bạn sẽ tự giúp mình nếu bạn quên tối ưu hóa và chỉ viết mã có thể đọc được (bạn sẽ cần chạy trình hồ sơ sau - nhưng không tối ưu hóa trước).


2
Thing thing();khai báo một hàm cục bộ và trả về a Thing.
dreamlax

2
Điều điều () tuyên bố một hàm trả về một điều. Không có đối tượng Thing được xây dựng trong cơ thể chức năng của bạn.
CB Bailey

@dreamlax @Charles @GMan Hơi muộn, nhưng đã sửa.
Amir Rachum

Làm thế nào điều này hoạt động trong C ++ 98? Tôi gặp lỗi trên trình thông dịch CINT và đã tự hỏi đó là do C ++ 98 hoặc chính CINT ...!
xcorat

16

Chỉ cần trả về một đối tượng như thế này:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Điều này sẽ gọi hàm tạo sao chép trên Things, vì vậy bạn có thể muốn thực hiện việc đó. Như thế này:

Thing(const Thing& aThing) {}

Điều này có thể thực hiện chậm hơn một chút, nhưng nó có thể không phải là một vấn đề.

Cập nhật

Trình biên dịch có thể sẽ tối ưu hóa cuộc gọi đến hàm tạo sao chép, do đó sẽ không có thêm chi phí. (Giống như dreamlax đã chỉ ra trong bình luận).


9
Thing thing();khai báo một hàm cục bộ trả về một Thing, đồng thời, tiêu chuẩn cho phép trình biên dịch bỏ qua hàm tạo sao chép trong trường hợp bạn đã trình bày; bất kỳ trình biên dịch hiện đại có thể sẽ làm điều đó.
dreamlax

1
Bạn mang lại một điểm tốt bằng cách triển khai hàm tạo sao chép, đặc biệt nếu cần một bản sao sâu.
mbadawi23

+1 để nêu rõ ràng về hàm tạo sao chép, mặc dù như @dreamlax nói rằng trình biên dịch có thể sẽ "tối ưu hóa" mã trả về cho các hàm tránh một cuộc gọi không thực sự cần thiết đến hàm tạo sao chép.
jose.angel.jimenez

Năm 2018, trong VS 2017, nó đang cố gắng sử dụng hàm tạo di chuyển. Nếu hàm tạo di chuyển bị xóa và hàm tạo sao chép thì không, nó sẽ không biên dịch.
Andrew

11

Bạn đã thử sử dụng con trỏ thông minh (nếu Thing thực sự là đối tượng lớn và nặng), như auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}

4
auto_ptrs bị phản đối; sử dụng shared_ptrhoặc unique_ptrthay thế.
MBraedley

Chỉ cần thêm cái này vào đây ... Tôi đã sử dụng c ++ trong nhiều năm, mặc dù không chuyên nghiệp với c ++ ... Tôi đã quyết định không sử dụng con trỏ thông minh nữa, chúng chỉ là một mớ hỗn độn tuyệt đối và gây ra tất cả Các loại vấn đề, không thực sự giúp tăng tốc mã nhiều. Tôi rất muốn thay vì chỉ sao chép dữ liệu xung quanh và tự mình quản lý con trỏ, sử dụng RAII. Vì vậy, tôi khuyên rằng nếu bạn có thể, tránh con trỏ thông minh.
Andrew

8

Một cách nhanh chóng để xác định xem hàm tạo sao chép có được gọi hay không là thêm ghi nhật ký vào hàm tạo sao chép của lớp bạn:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Gọi someFunction; số lượng dòng "Copy constructor được gọi là" mà bạn sẽ nhận được sẽ khác nhau giữa 0, 1 và 2. Nếu bạn không nhận được, thì trình biên dịch của bạn đã tối ưu hóa giá trị trả về (điều này được phép làm). Nếu bạn không nhận được 0 và trình xây dựng sao chép của bạn rất tốn kém, thì hãy tìm kiếm các cách khác để trả về các thể hiện từ các hàm của bạn.


1

Đầu tiên bạn có một lỗi trong mã, bạn có nghĩa là có Thing *thing(new Thing());và chỉ return thing;.

  • Sử dụng shared_ptr<Thing>. Deref nó như tho nó là một con trỏ. Nó sẽ bị xóa cho bạn khi tham chiếu cuối cùng đến Thingtrong phạm vi.
  • Giải pháp đầu tiên rất phổ biến trong các thư viện ngây thơ. Nó có một số hiệu suất và chi phí cú pháp, tránh nó nếu có thể
  • Chỉ sử dụng giải pháp thứ hai nếu bạn có thể đảm bảo sẽ không có ngoại lệ nào bị ném hoặc khi hiệu suất hoàn toàn quan trọng (bạn sẽ can thiệp vào C hoặc lắp ráp trước khi điều này thậm chí có liên quan).

0

Tôi chắc rằng một chuyên gia về C ++ sẽ đi kèm với một câu trả lời tốt hơn, nhưng cá nhân tôi thích cách tiếp cận thứ hai. Sử dụng con trỏ thông minh giúp giải quyết vấn đề quêndelete và như bạn nói, có vẻ sạch sẽ hơn là phải tạo một đối tượng trước khi sử dụng (và vẫn phải xóa nó nếu bạn muốn phân bổ nó trên heap).

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.