2016-09-04 16 views
11

Khi sử dụng tham chiếu chuyển tiếp, bạn có nên chuyển tiếp giá trị giống nhau cho nhiều hơn một hàm không? Hãy xem xét đoạn mã sau đây:Chuyển tiếp cùng một giá trị cho hai hoặc nhiều hàm

template<typename Container> 
constexpr auto 
front(Container&& c) 
-> typename Container::value_type 
{ return std::forward<Container>(c).front(); } 

template<typename Container> 
constexpr auto 
back(Container&& c) 
-> typename Container::value_type 
{ return std::forward<Container>(c).back(); } 

template<typename Container> 
constexpr auto 
get_corner(Container&& c) 
{ 
    return do_something(front(std::forward<Container(c)), 
         back(std::forward<Container>(c)); 
} 

Nếu Container là tham chiếu giá trị, hàm này hoạt động tốt. Tuy nhiên, tôi lo lắng về các tình huống mà giá trị được chuyển cho nó, bởi vì giá trị sẽ bị tổn thương khi một hoạt động di chuyển xảy ra. Nghi ngờ của tôi là: Có cách nào đúng để chuyển tiếp container trong trường hợp đó, mà không làm mất danh mục giá trị?

+0

"bạn nên chuyển tiếp cùng một giá trị cho nhiều chức năng? Hãy xem xét đoạn mã sau đây: "Nói chung, có. Trong một số trường hợp sử dụng cụ thể, nó vô hại. Nó chỉ có thể làm tổn thương bạn, thực sự. Trong trường hợp sử dụng cụ thể của bạn, tại sao lại chuyển tiếp? – AndyG

+0

@AndyG, di chuyển ngữ nghĩa. –

Trả lời

13

Nói chung, không hợp lý cho cùng một chức năng để chuyển tiếp cùng một tham số hai lần. Không, trừ khi nó có kiến ​​thức cụ thể về những gì người nhận của tham số chuyển tiếp đó sẽ làm.

Hãy nhớ rằng: hành vi của std::forward có thể tương đương với hành vi của std::move, tùy thuộc vào thông số người dùng đã nhập. Và hành vi của xvalue sẽ phụ thuộc vào cách chức năng nhận xử lý nó. Nếu người nhận có tham chiếu không giá trị const, thì có khả năng là di chuyển từ giá trị đó nếu có thể. Điều đó sẽ để lại cho bạn một vật thể di chuyển. Nếu nó có giá trị, nó sẽ chắc chắn di chuyển từ giá trị đó nếu loại đó hỗ trợ.

Vì vậy, trừ khi bạn có kiến ​​thức cụ thể về hành vi mong đợi của các thao tác bạn đang sử dụng, không an toàn để chuyển tiếp tham số nhiều lần.

+0

Vì vậy, điều đúng đắn cần làm ở đây là chỉ chấp nhận một tham chiếu const lvalue đến một container? –

+4

@thist: Ở đây, bởi vì không phải 'front()' cũng không phải 'back()' nên bao giờ thực hiện một chuyển động, truyền tham chiếu const lvalue cho chúng là chính xác. Nói chung, cách tiếp cận đúng là 'func1 (rvaluerefparam); func2 (rvaluerefparam); func3 (std :: forward (rvaluerefparam)); 'Đó là, chỉ cho phép người dùng cuối cùng ăn cắp tài nguyên. –

+1

@thlst: 'forward' chỉ hữu ích để liên tục truyền cả tham chiếu và tham chiếu giá trị r; nếu bạn chỉ giới hạn vấn đề với các tham chiếu không có giá trị r (để ngăn chặn các chuyển động), thì bạn có thể chuyển cùng một tham chiếu (const hoặc không) cho nhiều hàm. Nếu có thể thay đổi, thứ tự các hàm được gọi là vật chất. –

4

Có thực sự không có phiên bản rvalue tham chiếu của std::begin - chúng tôi chỉ có (đặt sang một bên constexpr và trở về giá trị):

template <class C> 
??? begin(C&); 

template <class C> 
??? begin(C const&); 

Đối với container vế trái, bạn sẽ có được iterator, và cho container rvalue, bạn sẽ có được const_iterator (hoặc bất cứ thứ gì tương đương với container cụ thể sẽ kết thúc).

Sự cố thực sự trong mã của bạn là trả lại decltype(auto). Đối với các hộp chứa lvalue, điều đó tốt - bạn sẽ trả về một tham chiếu đến một đối tượng có thời gian tồn tại vượt quá hàm. Nhưng đối với các thùng chứa rvalue, đó là trả lại một tham chiếu lơ lửng. Bạn sẽ muốn trả lại tham chiếu cho các hộp chứa lvalue và một giá trị cho các vùng chứa giá trị.

Ngày đầu đó, forward -ing các container vào begin()/end() có lẽ không phải những gì bạn muốn làm. Sẽ có hiệu quả hơn nếu điều kiện kết quả của select() là một trình chuyển đổi di chuyển. Một cái gì đó như this answer of mine:

template <typename Container, 
      typename V = decltype(*std::begin(std::declval<Container&>())), 
      typename R = std::conditional_t< 
       std::is_lvalue_reference<Container>::value, 
       V, 
       std::remove_reference_t<V> 
       > 
      > 
constexpr R operator()(Container&& c) 
{ 
    auto it = select(std::begin(c), std::end(c)); 
    return *make_forward_iterator<Container>(it); 
} 

Có thể có một cách tiết kiệm hơn để thể hiện tất cả điều đó.

+1

Ok, nhưng điều đó không cho thấy cách chuyển tiếp cùng một giá trị cho nhiều chức năng hơn. –

+0

@thlst Được rồi, nhưng câu hỏi của bạn là về đoạn mã bạn đã đăng. Nếu bạn muốn đặt câu hỏi về một đoạn mã khác, hãy đăng một đoạn mã khác? – Barry

+3

@Barry: Câu hỏi của ông khá rõ ràng; mã chỉ là * một ví dụ *, một đoạn mã có thể. Anh ta không hỏi câu hỏi "mã này sẽ hoạt động". Anh ấy hỏi "có thể làm điều này, với một ví dụ" hay không. –

2

Nói chung, có, điều này rất nguy hiểm.

Chuyển tiếp tham số đảm bảo rằng nếu giá trị nhận được bởi tham số tham chiếu chung là giá trị của một số loại, nó sẽ tiếp tục là một giá trị khi nó được chuyển tiếp. Nếu giá trị cuối cùng được chuyển tiếp đến một hàm (chẳng hạn như di chuyển-constructor) mà tiêu thụ giá trị bằng cách di chuyển từ nó, trạng thái nội bộ của nó không có khả năng hợp lệ để sử dụng trong các cuộc gọi tiếp theo.

Nếu bạn không chuyển tiếp tham số, nó sẽ không (nói chung) đủ điều kiện cho các hoạt động di chuyển, vì vậy bạn sẽ được an toàn khỏi hành vi đó.

Trong trường hợp của bạn, frontback (cả chức năng miễn phí và chức năng thành viên) không thực hiện di chuyển trên vùng chứa, vì vậy ví dụ cụ thể bạn đưa ra phải an toàn. Tuy nhiên, điều này cũng chứng minh rằng không có lý do gì để chuyển tiếp container, vì một giá trị sẽ không được xử lý khác với giá trị - đó là lý do duy nhất để duy trì sự phân biệt bằng cách chuyển tiếp giá trị ngay từ đầu.

3

Bạn có lẽ nhận ra rằng bạn sẽ không muốn std::move một đối tượng được thông qua với nhiều chức năng:

std::string s = "hello"; 
std::string hello1 = std::move(s); 
std::string hello2 = std::move(s); // hello2 != "hello" 

Vai trò của forward chỉ đơn giản là để khôi phục lại bất kỳ tình trạng rvalue rằng một tham số có khi nó đã được thông qua để chức năng.

Chúng tôi có thể nhanh chóng chứng minh rằng nó là xấu thực hành bởi forward ing một tham số hai lần để một chức năng mà có một hiệu ứng di chuyển:

#include <iostream> 
#include <string> 

struct S { 
    std::string name_ = "defaulted"; 
    S() = default; 
    S(const char* name) : name_(name) {} 
    S(S&& rhs) { std::swap(name_, rhs.name_); name_ += " moved"; } 
}; 

void fn(S s) 
{ 
    std::cout << "fn(" << s.name_ << ")\n"; 
} 

template<typename T> 
void fwd_test(T&& t) 
{ 
    fn(std::forward<T>(t)); 
    fn(std::forward<T>(t)); 
} 

int main() { 
    fwd_test(S("source")); 
} 

http://ideone.com/NRM8Ph

Nếu chuyển tiếp là an toàn, chúng ta sẽ thấy fn(source moved) hai lần, nhưng thay vào đó chúng ta thấy:

fn(source moved) 
fn(defaulted moved) 
Các vấn đề liên quan