Ưu điểm của giá trị chuyển qua và std :: di chuyển qua tham chiếu chuyển qua


105

Tôi đang học C ++ và cố gắng tránh mắc phải những thói quen xấu. Theo những gì tôi hiểu, clang-slim chứa nhiều "phương pháp hay nhất" và tôi cố gắng tuân theo chúng tốt nhất có thể (mặc dù tôi không nhất thiết phải hiểu tại sao chúng được coi là tốt), nhưng tôi không chắc liệu mình có hiểu những gì được đề xuất ở đây.

Tôi đã sử dụng lớp này từ hướng dẫn:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

Điều này dẫn đến một gợi ý từ clang-slim rằng tôi nên chuyển theo giá trị thay vì tham chiếu và sử dụng std::move. Nếu tôi làm, tôi nhận được gợi ý để làm cho namemột tài liệu tham khảo (để đảm bảo nó không được sao chép mỗi lần) và cảnh báo rằng std::movesẽ không có bất kỳ tác vì namelà một constvì vậy tôi nên loại bỏ nó.

Cách duy nhất tôi không nhận được cảnh báo là xóa consthoàn toàn:

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Điều này có vẻ hợp lý, vì lợi ích duy nhất của nó constlà ngăn chặn sự xáo trộn với chuỗi ban đầu (điều này không xảy ra vì tôi đã chuyển theo giá trị). Nhưng tôi đọc trên CPlusPlus.com :

Mặc dù lưu ý rằng -trong thư viện chuẩn- move ngụ ý rằng đối tượng được chuyển đến được để ở trạng thái hợp lệ nhưng không xác định. Có nghĩa là, sau một thao tác như vậy, giá trị của đối tượng được chuyển từ chỉ nên bị hủy hoặc gán một giá trị mới; truy cập nó nếu không sẽ mang lại một giá trị không xác định.

Bây giờ hãy tưởng tượng mã này:

std::string nameString("Alex");
Creature c(nameString);

Bởi vì nameStringđược truyền theo giá trị, std::movesẽ chỉ làm mất hiệu lực namebên trong hàm tạo và không chạm vào chuỗi ban đầu. Nhưng những lợi thế của điều này là gì? Có vẻ như nội dung chỉ được sao chép một lần - nếu tôi chuyển qua tham chiếu khi tôi gọi m_name{name}, nếu tôi chuyển theo giá trị khi tôi chuyển nó (và sau đó nó được di chuyển). Tôi hiểu rằng điều này tốt hơn là chuyển theo giá trị và không sử dụng std::move(vì nó được sao chép hai lần).

Vì vậy, hai câu hỏi:

  1. Tôi đã hiểu đúng những gì đang xảy ra ở đây?
  2. Có bất kỳ lợi ích nào của việc sử dụng std::movechuyển qua tham chiếu và chỉ gọi m_name{name}không?

3
Với lượt chuyển qua tham chiếu, hãy Creature c("John");tạo thêm một bản sao
user253751

1
Liên kết này có thể là một bài đọc có giá trị, nó bao gồm việc chuyển qua std::string_viewvà cả SSO.
lubgr

Tôi đã tìm thấy clang-tidylà một cách tuyệt vời để khiến bản thân bị ám ảnh bởi những kích thích vi mô không cần thiết với chi phí là khả năng đọc được. Câu hỏi cần đặt ra ở đây, trước bất kỳ điều gì khác, là chúng ta thực sự gọi hàm Creaturetạo bao nhiêu lần .
cz

Câu trả lời:


36
  1. Tôi đã hiểu đúng những gì đang xảy ra ở đây?

Đúng.

  1. Có bất kỳ lợi ích nào của việc sử dụng std::movechuyển qua tham chiếu và chỉ gọi m_name{name}không?

Một chữ ký chức năng dễ nắm bắt mà không có bất kỳ quá tải bổ sung nào. Chữ ký ngay lập tức tiết lộ rằng đối số sẽ được sao chép - điều này giúp người gọi khỏi băn khoăn liệu một const std::string&tham chiếu có thể được lưu trữ dưới dạng thành viên dữ liệu hay không, có thể trở thành một tham chiếu lơ lửng sau này. Và không cần phải nạp chồng vào std::string&& namevà các const std::string&đối số để tránh các bản sao không cần thiết khi các giá trị được truyền cho hàm. Vượt qua một giá trị

std::string nameString("Alex");
Creature c(nameString);

đối với hàm nhận đối số theo giá trị của nó sẽ tạo ra một bản sao và một cấu trúc di chuyển. Truyền một giá trị cho cùng một hàm

std::string nameString("Alex");
Creature c(std::move(nameString));

gây ra hai cấu trúc di chuyển. Ngược lại, khi có tham số hàm const std::string&, sẽ luôn có một bản sao, ngay cả khi truyền đối số rvalue. Đây rõ ràng là một lợi thế miễn là kiểu đối số rẻ để di chuyển-cấu trúc (trường hợp này xảy ra std::string).

Nhưng có một nhược điểm cần xem xét: lập luận không hoạt động đối với các hàm gán đối số hàm cho một biến khác (thay vì khởi tạo nó):

void setName(std::string name)
{
    m_name = std::move(name);
}

sẽ gây ra sự phân bổ tài nguyên m_nametham chiếu đến trước khi nó được chỉ định lại. Tôi khuyên bạn nên đọc Mục 41 trong C ++ Hiện đại Hiệu quả và cả câu hỏi này .


Điều đó có ý nghĩa, đặc biệt là điều này làm cho khai báo trực quan hơn để đọc. Tôi không chắc mình hoàn toàn nắm được phần thỏa thuận trong câu trả lời của bạn (và hiểu chuỗi được liên kết), vì vậy chỉ cần kiểm tra Nếu tôi sử dụng move, không gian sẽ được phân bổ. Nếu tôi không sử dụng move, nó chỉ được phân bổ nếu không gian được phân bổ quá nhỏ để chứa chuỗi mới, dẫn đến cải thiện hiệu suất. Đúng không?
Blackbot

1
Vâng, chính xác là như vậy. Khi gán cho m_nametừ một const std::string&tham số, bộ nhớ trong được sử dụng lại miễn là m_namephù hợp. Khi chuyển giao m_name, bộ nhớ phải được phân bổ trước. Nếu không, không thể "đánh cắp" tài nguyên từ phía bên phải của nhiệm vụ.
lubgr

Khi nào nó trở thành một tham chiếu lủng lẳng? Tôi nghĩ rằng danh sách khởi tạo sử dụng bản sao sâu.
Li Taiji

101
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • Giá trị được truyền vào liên kết với name, sau đó được sao chép vào m_name.

  • Một truyền rvalue liên kết với name, sau đó được sao chép vào m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Giá trị đã qua được sao chép vào name, sau đó được chuyển vào m_name.

  • Một truyền rvalue được chuyển vào name, sau đó được chuyển vào m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • Giá trị được truyền vào liên kết với name, sau đó được sao chép vào m_name.

  • Một truyền rvalue liên kết với rname, sau đó được chuyển vào m_name.


Vì các thao tác di chuyển thường nhanh hơn các bản sao, (1) tốt hơn (0) nếu bạn vượt qua nhiều thời gian tạm thời. (2) là tối ưu về bản sao / di chuyển, nhưng yêu cầu lặp lại mã.

Việc lặp lại mã có thể tránh được với tính năng chuyển tiếp hoàn hảo :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

Bạn có thể tùy ý muốn ràng buộc Tđể hạn chế miền của các kiểu mà hàm tạo này có thể được khởi tạo với (như được hiển thị ở trên). C ++ 20 nhằm mục đích đơn giản hóa điều này với các khái niệm .


Trong C ++ 17, các giá trị prvalues bị ảnh hưởng bởi việc xóa bản sao được đảm bảo , điều này - khi có thể - sẽ làm giảm số lượng bản sao / di chuyển khi truyền đối số cho các hàm.


Đối với (1) trường hợp pr-value và xvalue không giống nhau vì c ++ 17 không?
Oliv

1
Lưu ý rằng bạn không cần SFINAE để chuyển tiếp hoàn hảo trong trường hợp này. Nó chỉ cần thiết để phân biệt. Đó là plausibly hữu ích cho các thông báo lỗi tiềm năng khi đi qua lập luận xấu
Caleth

@Oliv Có. xvalues cần phải được di chuyển, trong khi prvalues có thể được ellided đi :)
Rakete1111

1
Chúng ta có thể viết: Creature(const std::string &name) : m_name{std::move(name)} { }trong (2) ?
skytree 19/02/19

4
@skytree: bạn không thể di chuyển từ một đối tượng const, vì việc di chuyển làm thay đổi nguồn. Điều đó sẽ biên dịch, nhưng nó sẽ tạo ra một bản sao.
Vittorio Romeo

1

Cách bạn vượt qua không phải là biến duy nhất ở đây, những gì bạn vượt qua tạo ra sự khác biệt lớn giữa hai điều này.

Trong C ++, chúng tôi có tất cả các loại các loại giá trị và điều này "thành ngữ" hiện hữu đối với trường hợp bạn vượt qua trong một rvalue (như "Alex-string-literal-that-constructs-temporary-std::string"hay std::move(nameString)), mà kết quả trong 0 bản sao của std::stringđang được thực hiện (loại thậm chí không cần phải sao chép constructible cho các đối số rvalue), và chỉ sử dụng hàm tạo std::stringdi chuyển của.

Hỏi & Đáp có liên quan .


1

Có một số nhược điểm của phương pháp tiếp cận chuyển theo giá trị và di chuyển qua tham chiếu chuyển qua (rv):

  • nó làm cho 3 đối tượng được sinh ra thay vì 2;
  • truyền một đối tượng theo giá trị có thể dẫn đến chi phí ngăn xếp bổ sung, bởi vì ngay cả lớp chuỗi thông thường thường lớn hơn con trỏ ít nhất 3 hoặc 4 lần;
  • Việc xây dựng các đối tượng đối số sẽ được thực hiện ở phía người gọi, gây ra sự phình to mã;

Bạn có thể làm rõ tại sao nó lại khiến 3 vật thể xuất hiện? Từ những gì tôi hiểu, tôi có thể chuyển "Peter" dưới dạng một chuỗi. Điều này sẽ được tạo ra, sao chép và sau đó di chuyển, phải không? Và không phải lúc nào đó ngăn xếp sẽ được sử dụng? Không phải ở điểm của lời gọi hàm tạo, mà ở m_name{name}phần nơi nó được sao chép?
Blackbot

@Blackbot Tôi đang đề cập đến ví dụ của bạn, std::string nameString("Alex"); Creature c(nameString);một đối tượng là nameString, một đối tượng khác là đối số hàm và đối tượng thứ ba là trường lớp.
user7860670

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.