Cách hiệu quả để trả về một vectơ std :: trong c ++


106

Bao nhiêu dữ liệu được sao chép, khi trả về một vectơ std :: trong một hàm và mức độ tối ưu hóa sẽ lớn đến mức nào khi đặt vectơ std :: trong kho lưu trữ miễn phí (trên heap) và trả về một con trỏ thay thế tức là:

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

hiệu quả hơn:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 

?


3
Làm thế nào về việc truyền vector theo tham chiếu và sau đó điền nó vào bên trong f?
Kiril Kirov,

4
RVO là một tối ưu hóa khá cơ bản mà hầu hết các trình biên dịch sẽ có thể thực hiện bất cứ lúc nào.
Remus Rusanu

Khi câu trả lời đến, nó có thể giúp bạn làm rõ liệu bạn đang sử dụng C ++ 03 hay C ++ 11. Các phương pháp hay nhất giữa hai phiên bản khác nhau khá nhiều.
Drew Dormann


@Kiril Kirov, Tôi có thể làm điều đó mà không đưa nó vào danh sách đối số của hàm tức là. void f (std :: vector & result)?
Morten

Câu trả lời:


140

Trong C ++ 11, đây là cách ưu tiên:

std::vector<X> f();

Đó là, trả về theo giá trị.

Với C ++ 11, std::vectorcó ngữ nghĩa di chuyển, có nghĩa là vectơ cục bộ được khai báo trong hàm của bạn sẽ được di chuyển trở lại và trong một số trường hợp, trình biên dịch thậm chí có thể làm rõ việc di chuyển.


13
@LeonidVolnitsky: Có nếu đó là địa phương . Trên thực tế, return std::move(v);sẽ vô hiệu hóa chuyển động ngay cả khi nó có thể thực hiện được return v;. Vì vậy, cái sau được ưu tiên hơn.
Nawaz

1
@juanchopanza: Tôi không nghĩ vậy. Trước C ++ 11, bạn có thể phản đối nó vì vector sẽ không được di chuyển; và RVO là một thứ phụ thuộc vào trình biên dịch! Nói về những thứ từ những năm 80 và 90.
Nawaz

2
Hiểu biết của tôi về giá trị trả về (theo giá trị) là: thay vì 'được di chuyển', giá trị trả về trong thư viện được tạo trên ngăn xếp của người gọi, vì vậy tất cả các hoạt động trong thư viện đều được thực hiện tại chỗ, không có gì để di chuyển trong RVO . Đúng không?
r0ng

2
@ r0ng: Vâng, đó là sự thật. Đó là cách mà các trình biên dịch thường thực hiện RVO.
Nawaz

1
@Nawaz Không phải vậy. Thậm chí không còn một động thái nào nữa.
Các cuộc đua ánh sáng trong quỹ đạo

70

Bạn nên trả về theo giá trị.

Tiêu chuẩn có một tính năng cụ thể để cải thiện hiệu quả của việc trả lại theo giá trị. Nó được gọi là "copy elision", và cụ thể hơn trong trường hợp này là "tối ưu hóa giá trị trả về được đặt tên (NRVO)".

Các trình biên dịch không phải triển khai nó, nhưng sau đó một lần nữa các trình biên dịch không phải triển khai nội tuyến hàm (hoặc thực hiện bất kỳ tối ưu hóa nào). Nhưng hiệu suất của các thư viện tiêu chuẩn có thể khá kém nếu trình biên dịch không tối ưu hóa và tất cả các trình biên dịch nghiêm túc thực hiện nội tuyến và NRVO (và các tối ưu hóa khác).

Khi NRVO được áp dụng, sẽ không có bản sao trong mã sau:

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

Nhưng người dùng có thể muốn làm điều này:

std::vector<int> myvec;
... some time later ...
myvec = f();

Copy elision không ngăn một bản sao ở đây vì nó là một nhiệm vụ chứ không phải là một khởi tạo. Tuy nhiên, bạn vẫn nên trả lại theo giá trị. Trong C ++ 11, phép gán được tối ưu hóa bằng một thứ khác, được gọi là "chuyển ngữ nghĩa". Trong C ++ 03, đoạn mã trên không gây ra một bản sao và mặc dù về lý thuyết, trình tối ưu hóa có thể tránh nó, nhưng trên thực tế, nó quá khó. Vì vậy, thay vì myvec = f(), trong C ++ 03, bạn nên viết như sau:

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

Có một tùy chọn khác, đó là cung cấp giao diện linh hoạt hơn cho người dùng:

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

Sau đó, bạn cũng có thể hỗ trợ giao diện dựa trên vectơ hiện có trên đó:

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

Điều này thể kém hiệu quả hơn mã hiện tại của bạn, nếu mã hiện tại của bạn sử dụng reserve()theo cách phức tạp hơn chỉ một số tiền cố định trả trước. Nhưng nếu mã hiện tại của bạn về cơ bản gọi push_backnhiều lần vào vectơ, thì mã dựa trên mẫu này phải tốt như vậy.


Bình chọn câu trả lời thực sự tốt nhất và chi tiết. Tuy nhiên, trong biến thể swap () của bạn ( đối với C ++ 03 không có NRVO ), bạn vẫn sẽ có một bản copy-constructor được tạo bên trong f (): từ kết quả biến thành một đối tượng tạm thời bị ẩn sẽ được hoán đổi lần cuối thành myvec .
JenyaKh

@JenyaKh: chắc chắn, đó là vấn đề về chất lượng triển khai. Tiêu chuẩn không yêu cầu triển khai C ++ 03 phải triển khai NRVO, giống như nó không yêu cầu nội tuyến hàm. Sự khác biệt so với nội tuyến hàm là nội tuyến không thay đổi ngữ nghĩa hoặc chương trình của bạn trong khi NRVO thì có. Mã di động phải hoạt động có hoặc không có NRVO. Mã được tối ưu hóa cho một triển khai cụ thể (và các cờ trình biên dịch cụ thể) có thể tìm kiếm sự đảm bảo về NRVO trong tài liệu riêng của triển khai.
Steve Jessop,

3

Đã đến lúc tôi đăng câu trả lời về RVO , tôi cũng vậy ...

Nếu bạn trả về một đối tượng theo giá trị, trình biên dịch thường tối ưu hóa điều này để nó không được xây dựng hai lần, vì việc xây dựng nó trong hàm dưới dạng tạm thời và sau đó sao chép nó là không cần thiết. Đây được gọi là tối ưu hóa giá trị trả về: đối tượng được tạo sẽ được di chuyển thay vì được sao chép.


1

Một thành ngữ phổ biến trước C ++ 11 là chuyển một tham chiếu đến đối tượng được điền.

Sau đó, không có bản sao của vector.

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 

3
Đó không còn là một thành ngữ trong C ++ 11.
Nawaz

1
@Nawaz Tôi đồng ý. Tôi không chắc thực tiễn tốt nhất hiện nay là gì về SO liên quan đến các câu hỏi về C ++ nhưng không cụ thể là C ++ 11. Tôi nghi ngờ mình nên đưa ra câu trả lời C ++ 11 cho một sinh viên, câu trả lời C ++ 03 cho một người nào đó am hiểu sâu về mã sản xuất. Bạn có ý kiến ​​gì?
Drew Dormann

7
Trên thực tế, sau khi phát hành C ++ 11 (được 19 tháng), tôi coi mọi câu hỏi đều là câu hỏi C ++ 11, trừ khi nó được nêu rõ ràng là câu hỏi C ++ 03.
Nawaz

1

Nếu trình biên dịch hỗ trợ Tối ưu hóa giá trị trả về được đặt tên ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ), bạn có thể trực tiếp trả về vectơ miễn là không có:

  1. Các đường dẫn khác nhau trả về các đối tượng được đặt tên khác nhau
  2. Nhiều đường dẫn trả về (ngay cả khi cùng một đối tượng được đặt tên được trả về trên tất cả các đường dẫn) với trạng thái EH được đưa vào.
  3. Đối tượng được đặt tên được trả về được tham chiếu trong một khối asm nội tuyến.

NRVO tối ưu hóa các lệnh gọi hàm tạo và hủy sao chép dư thừa và do đó cải thiện hiệu suất tổng thể.

Không nên có sự khác biệt thực sự trong ví dụ của bạn.


0
vector<string> getseq(char * db_file)

Và nếu bạn muốn in nó trên main (), bạn nên thực hiện nó trong một vòng lặp.

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}

-2

Có thể tốt như "trả về theo giá trị", đó là loại mã có thể dẫn đến lỗi. Hãy xem xét chương trình sau:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • Q: Điều gì sẽ xảy ra khi điều trên được thực hiện? A: Một cái lỗ nhỏ.
  • Q: Tại sao trình biên dịch không bắt được lỗi? A: Bởi vì chương trình là cú pháp, mặc dù không đúng về mặt ngữ nghĩa, nhưng đúng.
  • Hỏi: Điều gì xảy ra nếu bạn sửa đổi vecFunc () để trả về một tham chiếu? A: Chương trình chạy đến khi hoàn thành và tạo ra kết quả mong đợi.
  • Q: Sự khác biệt là gì? A: Trình biên dịch không phải tạo và quản lý các đối tượng ẩn danh. Lập trình viên đã hướng dẫn trình biên dịch sử dụng chính xác một đối tượng cho trình lặp và để xác định điểm cuối, thay vì hai đối tượng khác nhau như ví dụ bị hỏng.

Chương trình có lỗi ở trên sẽ chỉ ra không có lỗi ngay cả khi người ta sử dụng các tùy chọn báo cáo GNU g ++ -Wall -Wextra -Weffc ++

Nếu bạn phải tạo ra một giá trị, thì cách sau sẽ hoạt động thay cho việc gọi vecFunc () hai lần:

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

Ở trên cũng không tạo ra các đối tượng ẩn danh trong quá trình lặp lại vòng lặp, nhưng yêu cầu hoạt động sao chép có thể xảy ra (theo một số lưu ý, có thể được tối ưu hóa trong một số trường hợp. Nhưng phương pháp tham chiếu đảm bảo rằng sẽ không có bản sao nào được tạo ra. Tin rằng trình biên dịch sẽ thực hiện RVO không thể thay thế cho việc cố gắng xây dựng mã hiệu quả nhất mà bạn có thể. Nếu bạn có thể đề xuất sự cần thiết của trình biên dịch để thực hiện RVO, bạn đang dẫn đầu trò chơi.


3
Đây là một ví dụ về những gì có thể xảy ra nếu người dùng không quen với C ++ nói chung. Ai đó quen thuộc với các ngôn ngữ dựa trên đối tượng như .net hoặc javascript có thể sẽ cho rằng vectơ chuỗi luôn được truyền dưới dạng con trỏ và do đó trong ví dụ của bạn sẽ luôn trỏ đến cùng một đối tượng. vecfunc (). begin () và vecfunc (). end () sẽ không nhất thiết phải khớp trong ví dụ của bạn vì chúng phải là bản sao của vectơ chuỗi.
Medran

-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
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.