CÂU HỎI QUAN TRỌNG NHẤT ĐẦU TIÊN:
Có bất kỳ phương pháp hay nhất nào về cách gửi tham số trong C ++ không vì tôi thực sự tìm thấy nó, giả sử, không hề tầm thường
Nếu hàm của bạn cần sửa đổi đối tượng gốc đang được truyền, để sau khi cuộc gọi trả về, các sửa đổi đối với đối tượng đó sẽ được hiển thị cho người gọi, thì bạn nên chuyển bằng tham chiếu giá trị :
void foo(my_class& obj)
{
// Modify obj here...
}
Nếu hàm của bạn không cần sửa đổi đối tượng gốc và không cần tạo bản sao của nó (nói cách khác, nó chỉ cần quan sát trạng thái của nó), thì bạn nên chuyển bằng tham chiếu giá trị đếnconst
:
void foo(my_class const& obj)
{
// Observe obj here
}
Điều này sẽ cho phép bạn gọi hàm với cả giá trị (giá trị là đối tượng có danh tính ổn định) và với giá trị (giá trị, ví dụ: giá trị tạm thời hoặc đối tượng bạn sắp di chuyển từ kết quả của việc gọi std::move()
).
Người ta cũng có thể tranh luận rằng đối với các kiểu hoặc kiểu cơ bản mà việc sao chép diễn ra nhanh chóng , chẳng hạn như int
, bool
hoặc char
, không cần chuyển qua tham chiếu nếu hàm chỉ cần quan sát giá trị và chuyển theo giá trị nên được ưu tiên . Điều đó đúng nếu ngữ nghĩa tham chiếu là không cần thiết, nhưng điều gì sẽ xảy ra nếu hàm muốn lưu trữ một con trỏ đến đối tượng đầu vào giống nhau đó ở đâu đó, để sau này đọc qua con trỏ đó sẽ thấy các sửa đổi giá trị đã được thực hiện trong một số phần khác của mã? Trong trường hợp này, chuyển qua tham chiếu là giải pháp chính xác.
Nếu hàm của bạn không cần sửa đổi đối tượng gốc, nhưng cần lưu trữ một bản sao của đối tượng đó ( có thể trả về kết quả của một phép biến đổi đầu vào mà không làm thay đổi đầu vào ), thì bạn có thể xem xét lấy theo giá trị :
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
Việc gọi hàm trên sẽ luôn dẫn đến một bản sao khi chuyển các giá trị và trong một lần di chuyển khi chuyển các giá trị. Nếu hàm của bạn cần lưu trữ đối tượng này ở đâu đó, bạn có thể thực hiện thêm một lần di chuyển từ nó (ví dụ: trong trường hợp foo()
là một hàm thành viên cần lưu trữ giá trị trong một thành viên dữ liệu ).
Trong trường hợp việc di chuyển là tốn kém đối với các đối tượng cùng loại my_class
, thì bạn có thể cân nhắc nạp chồng foo()
và cung cấp một phiên bản cho giá trị (chấp nhận tham chiếu giá trị tới const
) và một phiên bản cho giá trị (chấp nhận tham chiếu giá trị ):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
Trên thực tế, các hàm trên rất giống nhau, đến mức bạn có thể tạo ra một hàm duy nhất từ nó: foo()
có thể trở thành một mẫu hàm và bạn có thể sử dụng chuyển tiếp hoàn hảo để xác định xem một động thái hay một bản sao của đối tượng đang được chuyển sẽ được tạo nội bộ:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
Bạn có thể muốn tìm hiểu thêm về thiết kế này bằng cách xem bài nói chuyện này của Scott Meyers (chỉ cần lưu ý rằng thuật ngữ " Tài liệu tham khảo chung " mà anh ấy đang sử dụng là không chuẩn).
Một điều cần lưu ý là std::forward
thường sẽ kết thúc bằng một lần di chuyển cho các giá trị, vì vậy mặc dù nó trông tương đối vô tội, việc chuyển tiếp cùng một đối tượng nhiều lần có thể là một nguồn rắc rối - ví dụ: di chuyển từ cùng một đối tượng hai lần! Vì vậy, hãy cẩn thận không đặt điều này trong một vòng lặp và không chuyển tiếp cùng một đối số nhiều lần trong một lời gọi hàm:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
Cũng lưu ý rằng bạn thường không sử dụng giải pháp dựa trên mẫu trừ khi bạn có lý do chính đáng cho nó, vì nó làm cho mã của bạn khó đọc hơn. Thông thường, bạn nên tập trung vào sự rõ ràng và đơn giản .
Trên đây chỉ là những hướng dẫn đơn giản, nhưng phần lớn chúng sẽ hướng bạn đến những quyết định thiết kế tốt.
QUAN TÂM ĐĂNG KÝ CỦA BẠN:
Nếu tôi viết lại thành [...] thì sẽ có 2 lần di chuyển và không có bản sao.
Điều này LAF không đúng. Để bắt đầu, một tham chiếu rvalue không thể liên kết với một lvalue, vì vậy điều này sẽ chỉ biên dịch khi bạn đang chuyển một kiểu rvalue CreditCard
cho hàm tạo của mình. Ví dụ:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));
Nhưng nó sẽ không hoạt động nếu bạn cố gắng làm điều này:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Vì cc
là một giá trị và các tham chiếu giá trị không thể liên kết với các giá trị. Hơn nữa, khi ràng buộc một tham chiếu với một đối tượng, không có động thái nào được thực hiện : nó chỉ là một ràng buộc tham chiếu. Vì vậy, sẽ chỉ có một nước đi .
Vì vậy, dựa trên các hướng dẫn được cung cấp trong phần đầu tiên của câu trả lời này, nếu bạn quan tâm đến số lần di chuyển được tạo ra khi bạn lấy một CreditCard
giá trị theo giá trị, bạn có thể xác định hai quá tải hàm tạo, một lấy tham chiếu giá trị đến const
( CreditCard const&
) và một lấy một tham chiếu rvalue ( CreditCard&&
).
Phân giải quá tải sẽ chọn cái trước khi chuyển một giá trị (trong trường hợp này, một bản sao sẽ được thực hiện) và cái sau khi truyền một giá trị (trong trường hợp này, một lần di chuyển sẽ được thực hiện).
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
Việc sử dụng của bạn std::forward<>
thường được xem khi bạn muốn đạt được chuyển tiếp hoàn hảo . Trong trường hợp đó, phương thức khởi tạo của bạn thực sự sẽ là một mẫu phương thức khởi tạo và sẽ trông giống như sau
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
Theo một nghĩa nào đó, điều này kết hợp cả hai quá tải mà tôi đã hiển thị trước đây thành một hàm duy nhất: C
sẽ được suy luận là CreditCard&
trong trường hợp bạn đang chuyển một giá trị và do các quy tắc thu gọn tham chiếu, nó sẽ khiến hàm này được khởi tạo:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Điều này sẽ tạo ra một bản sao xây dựng của creditCard
, như bạn mong muốn. Mặt khác, khi một giá trị được chuyển qua, C
sẽ được suy ra là CreditCard
và thay vào đó, hàm này sẽ được khởi tạo:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Điều này sẽ gây ra một động thái-xây dựng của creditCard
, đó là những gì bạn muốn (vì giá trị được thông qua là một rvalue, và điều đó có nghĩa là chúng ta được phép di chuyển từ nó).