2015-05-07 35 views
21

Tôi có một chức năng mà sẽ thay đổi std::string& tài liệu tham khảo giá trị trái tại chỗ, trở về một tham chiếu đến các tham số đầu vào:Tôi có nên trả lại tham số tham chiếu rvalue bằng tham chiếu rvalue không?

std::string& transform(std::string& input) 
{ 
    // transform the input string 
    ... 

    return input; 
} 

Tôi có một hàm helper cho phép biến đổi inline cùng được thực hiện trên tài liệu tham khảo rvalue:

std::string&& transform(std::string&& input) 
{ 
    return std::move(transform(input)); // calls the lvalue reference version 
} 

Lưu ý rằng số trả về tham chiếu rvalue.

Tôi đã đọc một số câu hỏi về SO liên quan đến các tham chiếu rvalue trả về (ví dụ: herehere) và đã đi đến kết luận rằng đây là thực hành không tốt.

Từ những gì tôi đã đọc, có vẻ như sự đồng thuận là sau khi trở về giá trị rvalues, cộng thêm có tính đến RVO, chỉ cần trở lại theo giá trị sẽ là hiệu quả:

std::string transform(std::string&& input) 
{ 
    return transform(input); // calls the lvalue reference version 
} 

Tuy nhiên, tôi cũng đã đọc rằng trở về thông số chức năng ngăn ngừa sự tối ưu hóa RVO (ví dụ herehere)

Điều này dẫn tôi để tin rằng một bản sao sẽ xảy ra từ giá trị std::string& trở lại của phiên bản tài liệu tham khảo vế trái của transform(...) vào giá trị trả lại std::string.

Điều đó có đúng không?

Tốt hơn là giữ phiên bản std::string&& transform(...) của tôi?

+1

Như một lưu ý phụ, hàm ban đầu chấp nhận và trả về '&' s bình thường là khá khó chịu - nó làm biến đổi đối tượng được truyền cho nó, nhưng nó ngụy trang trông giống như một hàm thuần túy. Đó là một công thức cho sự hiểu lầm. Đây có thể là điều khiến cho việc tìm ra phương pháp rvalue của nó trở nên khó khăn. –

+0

Điểm của việc trả lại thứ gì đó mà người dùng đã có? Nó không giống như bạn đang chuyển đổi chuỗi cuộc gọi, phải không? – Drax

+0

@ Drax, còn về 'std :: cout << foo (biến đổi (get_str()));'? –

Trả lời

7

Không có câu trả lời đúng, nhưng việc trả lại theo giá trị sẽ an toàn hơn.

Tôi đã đọc một số câu hỏi về SO liên quan đến tham chiếu rvalue trả về và đã đi đến kết luận rằng đây là thực hành không tốt.

Trả về một tham chiếu đến một tham số foists một hợp đồng khi người gọi mà một trong hai

  1. Tham số không thể là một tạm thời (mà chỉ là những gì tài liệu tham khảo rvalue đại diện), hoặc
  2. Giá trị trả lại thắng không được giữ lại trong dấu chấm phẩy tiếp theo trong ngữ cảnh của người gọi (khi thời gian tạm dừng bị phá hủy).

Nếu người gọi vượt qua tạm thời và cố gắng lưu kết quả, họ sẽ có được tham chiếu đáng yêu.

Từ những gì tôi đã đọc, có vẻ như sự đồng thuận là vì giá trị trả về là rvalues, cộng thêm có tính đến RVO, chỉ cần trở lại theo giá trị sẽ là hiệu quả:

Trở bởi giá trị tăng thêm một hoạt động di chuyển-xây dựng. Chi phí này thường tỉ lệ với kích thước của vật thể. Trong khi quay lại bằng tham chiếu chỉ yêu cầu máy để đảm bảo rằng một địa chỉ nằm trong sổ đăng ký, việc trả về theo giá trị yêu cầu zeroing một vài con trỏ trong tham số std::string và đặt giá trị của chúng trong một số mới std::string.

Giá rẻ, nhưng không phải là không.

Hướng hiện tại được thực hiện bởi thư viện chuẩn, hơi ngạc nhiên, nhanh và không an toàn và trả lại tham chiếu. (Chức năng duy nhất tôi biết thực sự thực hiện điều này là std::get từ <tuple>.) Khi điều đó xảy ra, tôi đã trình bày a proposal cho ủy ban ngôn ngữ cốt lõi C++ đối với độ phân giải của vấn đề này, a revision đang hoạt động, và hôm nay tôi đã bắt đầu điều tra việc triển khai. Nhưng nó phức tạp, và không phải là một điều chắc chắn.

std::string transform(std::string&& input) 
{ 
    return transform(input); // calls the lvalue reference version 
} 

Trình biên dịch sẽ không tạo ra một move đây.Nếu input không phải là tham chiếu chút nào và bạn đã thực hiện return input;, nhưng không có lý do gì để tin rằng transform sẽ trả về input chỉ vì đó là tham số và sẽ không khấu trừ quyền sở hữu từ loại tham chiếu rvalue. (Xem C++ 14 §12.8/31-32.)

Bạn cần phải làm:

return std::move(transform(input)); 

hoặc tương đương

transform(input); 
return std::move(input); 
0

nếu câu hỏi của bạn là tối ưu hóa thuần túy theo định hướng, tốt nhất là đừng lo lắng về cách vượt qua hoặc trả về một đối số. trình biên dịch là đủ thông minh để strech mã của bạn vào một trong hai tham chiếu thuần túy đi qua, sao chép elision, chức năng inlining và thậm chí di chuyển ngữ nghĩa nếu đó là phương pháp nhanh nhất.

về cơ bản, di chuyển ngữ nghĩa có thể mang lại lợi ích cho bạn trong một số trường hợp bí truyền. giả sử tôi có một đối tượng ma trận giữ double** dưới dạng biến thành viên và con trỏ này trỏ đến một mảng hai chiều mờ là double. bây giờ hãy nói rằng tôi có biểu hiện này:
Matrix a = b+c;
một constructor sao chép (hoặc điều hành assigment, trong trường hợp này) sẽ nhận được số tiền của bc như một temorary, vượt qua nó như tham chiếu const, tái phân bổ m*n lượng doubles trên a con trỏ bên trong, sau đó, nó sẽ chạy trên a+b tổng hợp mảng và sẽ sao chép từng giá trị của nó. tính toán dễ dàng cho thấy rằng nó có thể mất đến O(nm) bước (có thể được generlized để O(n^2)). di chuyển ngữ nghĩa sẽ chỉ tái dây mà ẩn double** ra khỏi temprary thành a con trỏ bên trong. phải mất O(1).
bây giờ chúng ta hãy suy nghĩ về std::string trong giây lát: chuyển nó làm tham chiếu mất O(1) bước (lấy bộ nhớ, vượt qua nó, dereference nó vv, điều này không phải là tuyến tính trong bất kỳ loại nào). truyền nó như là tham chiếu giá trị r yêu cầu chương trình truyền nó như một tham chiếu, dây lại ẩn bên dưới C- char* chứa bộ đệm bên trong, rỗng bộ đệm gốc (hoặc hoán đổi giữa chúng), sao chép sizecapacity và nhiều hành động hơn. chúng ta có thể thấy rằng mặc dù chúng ta vẫn đang ở trong vùng O(1) - có thể có nhiều bước thực tế hơn so với chỉ đơn giản là chuyển nó như một tham chiếu thông thường.

tốt, sự thật là tôi đã không đánh giá nó, và các cuộc thảo luận ở đây hoàn toàn là lý thuyết. không bao giờ ít hơn, đoạn đầu tiên của tôi vẫn đúng. chúng tôi giả định nhiều thứ như nhà phát triển, nhưng trừ khi chúng tôi chuẩn bị mọi thứ cho đến chết - trình biên dịch đơn giản là biết rõ hơn chúng tôi trong 99% thời gian

đưa đối số này vào tài khoản, tôi muốn giữ nó làm tham chiếu và không di chuyển ngữ nghĩa vì nó tương thích với từ vựng và được hiểu nhiều hơn cho các nhà phát triển, những người chưa thành thạo C++ 11.

+2

Tôi đồng ý với bạn từ góc độ thực tế. Tuy nhiên, tôi nghĩ rằng đây vẫn là một câu hỏi rất tốt để giúp đỡ trong việc tìm hiểu tài liệu tham khảo rValue. Tôi đã làm việc với rValue refences cho một thời gian khá bây giờ nhưng khía cạnh này tôi vẫn muốn hiểu một chút tốt hơn :). – laurisvr

1

Một số (không đại diện) runtimes cho các phiên bản cao hơn của transform:

run on coliru

#include <iostream> 
#include <time.h> 
#include <sys/time.h> 
#include <unistd.h> 

using namespace std; 

double GetTicks() 
{ 
    struct timeval tv; 
    if(!gettimeofday (&tv, NULL)) 
     return (tv.tv_sec*1000 + tv.tv_usec/1000); 
    else 
     return -1; 
} 

std::string& transform(std::string& input) 
{ 
    // transform the input string 
    // e.g toggle first character 
    if(!input.empty()) 
    { 
     if(input[0]=='A') 
      input[0] = 'B'; 
     else 
      input[0] = 'A'; 
    } 
    return input; 
} 

std::string&& transformA(std::string&& input) 
{ 
    return std::move(transform(input)); 
} 

std::string transformB(std::string&& input) 
{ 
    return transform(input); // calls the lvalue reference version 
} 

std::string transformC(std::string&& input) 
{ 
    return std::move(transform(input)); // calls the lvalue reference version 
} 


string getSomeString() 
{ 
    return string("ABC"); 
} 

int main() 
{ 
    const int MAX_LOOPS = 5000000; 

    { 
     double start = GetTicks(); 
     for(int i=0; i<MAX_LOOPS; ++i) 
      string s = transformA(getSomeString()); 
     double end = GetTicks(); 

     cout << "\nRuntime transformA: " << end - start << " ms" << endl; 
    } 

    { 
     double start = GetTicks(); 
     for(int i=0; i<MAX_LOOPS; ++i) 
      string s = transformB(getSomeString()); 
     double end = GetTicks(); 

     cout << "\nRuntime transformB: " << end - start << " ms" << endl; 
    } 

    { 
     double start = GetTicks(); 
     for(int i=0; i<MAX_LOOPS; ++i) 
      string s = transformC(getSomeString()); 
     double end = GetTicks(); 

     cout << "\nRuntime transformC: " << end - start << " ms" << endl; 
    } 

    return 0; 
} 

đầu ra

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out 

Runtime transformA: 444 ms 
Runtime transformB: 796 ms 
Runtime transformC: 434 ms 
+0

Liệu biện pháp có thay đổi với các chuỗi lớn hơn (không phù hợp với SSO) không? – Hiura

+1

@Hiura: Chỉ cần theo liên kết _COLIRU_ ở trên, nhấn _Edit_ và thay đổi mã theo ý bạn và thử. –

0

Điều này dẫn tôi để tin rằng một bản sao sẽ xảy ra từ std :: string & giá trị trở lại của phiên bản tài liệu tham khảo vế trái của transform (...) vào std :: giá trị trả về chuỗi.

Điều đó có đúng không?

Phiên bản tham chiếu trả về sẽ không cho phép std :: sao chép chuỗi xảy ra, nhưng phiên bản giá trị trả về sẽ có bản sao, nếu trình biên dịch không thực hiện RVO. Tuy nhiên, RVO có giới hạn của nó, vì vậy C++ 11 thêm tham chiếu giá trị r và di chuyển constructor/assignment/std :: di chuyển để giúp xử lý tình huống này. Có, RVO là hiệu quả hơn di chuyển ngữ nghĩa, di chuyển là rẻ hơn so với bản sao nhưng đắt hơn RVO.

Tốt hơn là giữ phiên bản std :: string & & chuyển đổi (...)?

Điều này thật thú vị và kỳ lạ. Vì Potatoswatter đã trả lời,

std::string transform(std::string&& input) 
{ 
    return transform(input); // calls the lvalue reference version 
} 

Bạn nên gọi std :: move manually.

Tuy nhiên, bạn có thể nhấp vào liên kết nhà phát triển này: RVO V.S. std::move để xem thêm chi tiết, giải thích rõ vấn đề của bạn.

Các vấn đề liên quan