Keuntungan dari pass-by-value dan std :: move over pass-by-reference


107

Saya sedang belajar C ++ saat ini dan mencoba menghindari kebiasaan buruk. Dari apa yang saya pahami, clang-tidy mengandung banyak "praktik terbaik" dan saya berusaha mematuhinya sebaik mungkin (meskipun saya belum begitu mengerti mengapa mereka dianggap baik), tapi saya tidak yakin apakah saya memahami apa yang direkomendasikan di sini.

Saya menggunakan kelas ini dari tutorial:

class Creature
{
private:
    std::string m_name;

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

Ini mengarah pada saran dari clang-tidy bahwa saya harus meneruskan nilai alih-alih referensi dan penggunaan std::move. Jika saya melakukannya, saya mendapat saran untuk membuat namereferensi (untuk memastikan itu tidak disalin setiap waktu) dan peringatan yang std::movetidak akan berpengaruh apa pun karena nameitu constjadi saya harus menghapusnya.

Satu-satunya cara agar saya tidak mendapatkan peringatan adalah dengan menghapus constsemuanya:

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

Yang tampaknya logis, karena satu-satunya manfaat constadalah mencegah mengacaukan string asli (yang tidak terjadi karena saya meneruskan nilai). Tapi saya membaca di CPlusPlus.com :

Meskipun perhatikan bahwa -dalam perpustakaan standar- pemindahan menyiratkan bahwa objek yang dipindahkan dibiarkan dalam keadaan yang valid tetapi tidak ditentukan. Artinya, setelah operasi semacam itu, nilai objek yang dipindahkan hanya boleh dihancurkan atau diberi nilai baru; mengaksesnya jika tidak menghasilkan nilai yang tidak ditentukan.

Sekarang bayangkan kode ini:

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

Karena nameStringdilewatkan oleh nilai, std::movehanya akan membatalkan namedi dalam konstruktor dan tidak menyentuh string asli. Tapi apa keuntungannya? Sepertinya konten hanya disalin sekali - jika saya melewati referensi ketika saya menelepon m_name{name}, jika saya melewatkan nilai ketika saya melewatinya (dan kemudian dipindahkan). Saya mengerti bahwa ini lebih baik daripada melewatkan nilai dan tidak menggunakan std::move(karena akan disalin dua kali).

Jadi dua pertanyaan:

  1. Apakah saya memahami dengan benar apa yang terjadi di sini?
  2. Apakah ada keuntungan dari penggunaan std::movereferensi yang lewat dan hanya menelepon m_name{name}?

3
Dengan referensi lewat, Creature c("John");buat salinan tambahan
user253751

1
Tautan ini mungkin bacaan yang berharga, mencakup kelulusan std::string_viewdan SSO juga.
lubgr

Saya telah menemukan clang-tidycara yang bagus untuk membuat diri saya terobsesi dengan pengoptimalan mikro yang tidak perlu dengan mengorbankan keterbacaan. Pertanyaan yang harus ditanyakan di sini, sebelum yang lainnya, adalah berapa kali kita benar-benar memanggil Creaturekonstruktor.
cz

Jawaban:


37
  1. Apakah saya memahami dengan benar apa yang terjadi di sini?

Iya.

  1. Apakah ada keuntungan dari penggunaan std::movereferensi yang lewat dan hanya menelepon m_name{name}?

Tanda tangan fungsi yang mudah dipahami tanpa kelebihan beban tambahan. Tanda tangan segera mengungkapkan bahwa argumen akan disalin - ini menyelamatkan penelepon dari bertanya-tanya apakah const std::string&referensi mungkin disimpan sebagai anggota data, mungkin menjadi referensi yang menggantung di kemudian hari. Dan tidak perlu membebani argumen std::string&& namedan const std::string&untuk menghindari salinan yang tidak perlu saat nilai r diteruskan ke fungsi. Meneruskan nilai l

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

ke fungsi yang mengambil argumennya berdasarkan nilai menyebabkan satu salinan dan konstruksi satu gerakan. Meneruskan nilai r ke fungsi yang sama

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

menyebabkan dua konstruksi bergerak. Sebaliknya, jika parameter fungsi adalah const std::string&, akan selalu ada salinannya, bahkan saat meneruskan argumen nilai r. Ini jelas merupakan keuntungan selama tipe argumen murah untuk dipindahkan-konstruksi (ini kasusnya std::string).

Tetapi ada sisi negatif yang perlu dipertimbangkan: alasan tidak berfungsi untuk fungsi yang menetapkan argumen fungsi ke variabel lain (daripada menginisialisasi):

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

akan menyebabkan pembatalan alokasi sumber daya yang m_namemerujuk sebelum ditetapkan ulang. Saya merekomendasikan membaca Butir 41 dalam Effective Modern C ++ dan juga pertanyaan ini .


Masuk akal, terutama karena ini membuat pernyataan lebih intuitif untuk dibaca. Saya tidak yakin saya sepenuhnya memahami bagian deallocation dari jawaban Anda (dan memahami utas yang ditautkan), jadi hanya untuk memeriksa Jika saya menggunakan move, ruang akan dibatalkan alokasinya. Jika saya tidak menggunakan move, itu hanya akan dibatalkan alokasinya jika ruang yang dialokasikan terlalu kecil untuk menampung string baru, yang mengarah ke peningkatan kinerja. Apakah itu benar?
Blackbot

1
Ya, begitulah. Saat menetapkan ke m_namedari suatu const std::string&parameter, memori internal digunakan kembali selama m_namecocok. Saat menetapkan ke m_name, memori harus dibatalkan alokasinya terlebih dahulu. Jika tidak, tidak mungkin untuk "mencuri" sumber daya dari sisi kanan tugas.
lubgr

Kapan itu menjadi referensi yang menggantung? Saya pikir daftar inisialisasi menggunakan deep copy.
Li Taiji

104
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • Nilai l yang diteruskan mengikat name, lalu disalin ke m_name.

  • Nilai r yang diteruskan mengikat name, lalu disalin ke m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Sebuah berlalu lvalue yang disalin ke dalam name, kemudian pindah ke m_name.

  • Sebuah berlalu nilai p adalah pindah ke name, kemudian pindah ke m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • Nilai l yang diteruskan mengikat name, lalu disalin ke m_name.

  • Nilai r yang dilalui mengikat ke rname, lalu dipindahkan ke m_name.


Karena operasi pemindahan biasanya lebih cepat daripada salinan, (1) lebih baik daripada (0) jika Anda memberikan banyak temporer. (2) optimal dalam hal menyalin / memindahkan, tetapi membutuhkan pengulangan kode.

Pengulangan kode dapat dihindari dengan penerusan sempurna :

/* (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)} { }

Anda mungkin secara opsional ingin membatasi Tuntuk membatasi jenis domain yang dapat digunakan oleh konstruktor ini (seperti yang ditunjukkan di atas). C ++ 20 bertujuan untuk menyederhanakan ini dengan Konsep .


Di C ++ 17, prvalues dipengaruhi oleh penghapusan salinan yang dijamin , yang - jika berlaku - akan mengurangi jumlah salinan / pemindahan saat meneruskan argumen ke fungsi.


Untuk (1) kasus pr-value dan xvalue tidak identik karena c ++ 17 no?
Oliv

1
Perhatikan bahwa Anda tidak memerlukan SFINAE untuk maju sempurna dalam kasus ini. Ini hanya perlu disambiguasi. Ini masuk akal membantu untuk potensi pesan kesalahan saat melewati argumen buruk
Caleth

@Oiv Ya. xvalues ​​perlu dipindahkan, sementara prvalues ​​dapat dihilangkan :)
Rakete1111

1
Bisakah kita menulis: Creature(const std::string &name) : m_name{std::move(name)} { }di (2) ?
skytree

4
@skytree: Anda tidak dapat berpindah dari objek const, karena pemindahan mengubah sumbernya. Itu akan dikompilasi, tetapi itu akan membuat salinan.
Vittorio Romeo

1

Cara Anda mengoper bukan satu-satunya variabel di sini, apa yang Anda lewati membuat perbedaan besar di antara keduanya.

Dalam C ++, kita memiliki semua jenis kategori nilai dan ini "idiom" ada untuk kasus-kasus di mana Anda lulus dalam nilai p (seperti "Alex-string-literal-that-constructs-temporary-std::string"atau std::move(nameString)), yang menghasilkan 0 salinan dari std::stringyang dibuat (tipe bahkan tidak harus copy-constructible untuk argumen nilai r), dan hanya menggunakan std::stringkonstruktor move.

T&J yang agak terkait .


1

Ada beberapa kelemahan pendekatan pass-by-value-and-move over pass-by- (rv):

  • ini menyebabkan 3 objek muncul, bukan 2;
  • melewatkan sebuah objek dengan nilai dapat menyebabkan tambahan stack overhead, karena bahkan kelas string biasa biasanya setidaknya 3 atau 4 kali lebih besar dari sebuah pointer;
  • konstruksi objek argumen akan dilakukan di sisi pemanggil, menyebabkan kode membengkak;

Bisakah Anda menjelaskan mengapa hal itu menyebabkan 3 objek muncul? Dari apa yang saya pahami, saya bisa melewatkan "Peter" sebagai string. Ini akan muncul, disalin, dan kemudian dipindahkan, bukan? Dan bukankah tumpukan akan digunakan di beberapa titik? Bukan pada titik pemanggilan konstruktor, tetapi pada m_name{name}bagian di mana ia disalin?
Blackbot

@Blackbot Saya mengacu pada contoh Anda std::string nameString("Alex"); Creature c(nameString);satu objek nameString, yang lain adalah argumen fungsi, dan yang ketiga adalah bidang kelas.
pengguna7860670

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.