Mengapa tidak ada move-assignment / move-constructor?

89

Saya seorang programmer sederhana. Variabel anggota kelas saya paling sering terdiri dari tipe POD dan kontainer STL. Karena itu saya jarang harus menulis operator penugasan atau menyalin konstruktor, karena ini diterapkan secara default.

Tambahkan ke ini, jika saya menggunakan std::movepada objek yang tidak dapat dipindahkan, ini menggunakan operator penugasan, artinya std::movesangat aman.

Karena saya pemrogram sederhana, saya ingin memanfaatkan kapabilitas pemindahan tanpa menambahkan operator konstruktor / penugasan pemindahan ke setiap kelas yang saya tulis, karena kompiler dapat dengan mudah menerapkannya sebagai " this->member1_ = std::move(other.member1_);..."

Tetapi tidak (setidaknya tidak dalam Visual 2010), apakah ada alasan khusus untuk ini?

Lebih penting; apakah ada cara untuk mengatasi ini?

Pembaruan: Jika Anda melihat jawaban GManNickG, dia memberikan makro yang bagus untuk ini. Dan jika Anda tidak tahu, jika Anda menerapkan semantik bergerak, Anda dapat menghapus fungsi anggota swap.

Viktor Sehr
sumber
5
Anda tahu bahwa Anda dapat meminta kompiler untuk menghasilkan perpindahan default ctor
aaronman
3
std :: move tidak melakukan perpindahan, ia hanya melakukan cast dari nilai l ke nilai r. Pemindahan masih dilakukan oleh konstruktor pemindahan.
Owen Delahoy
1
Apa yang kamu bicarakan MyClass::MyClass(Myclass &&) = default;?
Sandburg
Ya, sekarang :)
Viktor Sehr

Jawaban:

76

Generasi implisit dari konstruktor pemindahan dan operator penugasan telah diperdebatkan dan ada revisi besar dalam draf terbaru dari Standar C ++, jadi kompiler yang tersedia saat ini kemungkinan besar akan berperilaku berbeda sehubungan dengan pembuatan implisit.

Untuk informasi lebih lanjut tentang riwayat masalah, lihat daftar makalah WG21 2010 dan cari "mov"

Spesifikasi saat ini (N3225, dari November) menyatakan (N3225 12,8 / 8):

Jika definisi kelas Xtidak secara eksplisit mendeklarasikan konstruktor move, seseorang akan secara implisit dideklarasikan sebagai default jika dan hanya jika

  • X tidak memiliki konstruktor salinan yang dideklarasikan pengguna, dan

  • X tidak memiliki operator penugasan salin yang dinyatakan pengguna,

  • X tidak memiliki operator tugas pemindahan yang dinyatakan pengguna,

  • X tidak memiliki destruktor yang dideklarasikan pengguna, dan

  • konstruktor pemindahan tidak akan secara implisit didefinisikan sebagai dihapus.

Ada bahasa serupa di 12.8 / 22 yang menentukan kapan operator penugasan pindah secara implisit dideklarasikan sebagai default. Anda dapat menemukan daftar lengkap perubahan yang dibuat untuk mendukung spesifikasi generasi pemindahan implisit saat ini di N3203: Memperketat kondisi untuk menghasilkan gerakan implisit , yang sebagian besar didasarkan pada salah satu resolusi yang diusulkan oleh makalah Bjarne Stroustrup N3201: Bergerak bersama .

James McNellis
sumber
4
Saya menulis artikel kecil dengan beberapa diagram yang menjelaskan hubungan implisit (pindah) konstruktor / penugasan di sini: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny
Ugh jadi setiap kali saya harus mendefinisikan destruktor kosong di kelas dasar polimorfik hanya untuk menetapkannya sebagai virtual, saya harus secara eksplisit mendefinisikan konstruktor pemindahan dan operator penugasan juga :(.
someguy
@ James McNellis: Itu adalah sesuatu yang saya coba sebelumnya, tetapi kompiler sepertinya tidak menyukainya. Saya akan memposting pesan kesalahan di balasan ini, tetapi setelah mencoba mereproduksi kesalahan, saya menyadari bahwa itu menyebutkannya cannot be defaulted *in the class body*. Jadi, saya mendefinisikan destruktor di luar dan berhasil :). Tapi menurutku agak aneh. Apakah ada yang punya penjelasan? Kompilernya adalah gcc 4.6.1
someguy
3
Mungkin kita bisa mendapatkan pembaruan untuk jawaban ini sekarang setelah C ++ 11 diratifikasi? Penasaran perilaku apa yang dimenangkan.
Joseph Garvin
2
@Guy Avraham: Saya pikir apa yang saya katakan (sudah 7 tahun) adalah bahwa jika saya memiliki destruktor yang dideklarasikan oleh pengguna (bahkan yang virtual kosong), tidak ada konstruktor pemindahan yang akan dinyatakan secara implisit sebagai default. Saya kira itu akan menghasilkan semantik salinan? (Saya belum menyentuh C ++ selama bertahun-tahun.) James McNellis kemudian berkomentar bahwa virtual ~D() = default;seharusnya berfungsi dan masih mengizinkan konstruktor pemindahan implisit.
someguy
13

Konstruktor pemindahan yang dihasilkan secara implisit telah dipertimbangkan untuk standar, tetapi bisa berbahaya. Lihat analisis Dave Abrahams .

Pada akhirnya, bagaimanapun, standar tersebut memang memasukkan generasi implisit dari konstruktor pemindahan dan operator penugasan pindahan, meskipun dengan daftar batasan yang cukup substansial:

Jika definisi kelas X tidak secara eksplisit mendeklarasikan konstruktor pemindahan, seseorang akan secara implisit dideklarasikan sebagai default jika dan hanya jika
- X tidak memiliki konstruktor salinan yang dideklarasikan pengguna,
- X tidak memiliki operator penugasan salinan yang dideklarasikan pengguna ,
- X tidak memiliki operator tugas pemindahan yang dideklarasikan pengguna,
- X tidak memiliki destruktor yang dideklarasikan pengguna, dan
- konstruktor pemindahan tidak akan secara implisit didefinisikan sebagai dihapus.

Namun, bukan itu saja ceritanya. Sebuah ctor dapat dideklarasikan, tetapi tetap didefinisikan sebagai dihapus:

Konstruktor salin / pindahkan yang dideklarasikan secara implisit adalah anggota publik sebaris kelasnya. Konstruktor salin / pindahkan default untuk kelas X didefinisikan sebagai dihapus (8.4.3) jika X memiliki:

- anggota varian dengan konstruktor terkait non-sepele dan X adalah kelas seperti gabungan,
- anggota data non-statis dari jenis kelas M (atau lariknya) yang tidak dapat disalin / dipindahkan karena resolusi berlebih (13.3), sebagai diterapkan ke konstruktor M yang sesuai, menghasilkan ambiguitas atau fungsi yang dihapus atau tidak dapat diakses dari konstruktor default,
- kelas dasar B langsung atau virtual yang tidak dapat disalin / dipindahkan karena resolusi berlebih (13.3), seperti yang diterapkan pada konstruktor terkait B , menghasilkan ambiguitas atau fungsi yang dihapus atau tidak dapat diakses dari konstruktor default,
- kelas basis langsung atau virtual atau anggota data non-statis dari suatu tipe dengan destruktor yang dihapus atau tidak dapat diakses dari konstruktor default,
- untuk konstruktor salinan, anggota data non-statis dari tipe referensi nilai r, atau
- untuk konstruktor pemindahan, anggota data non-statis atau kelas basis virtual atau langsung dengan tipe yang tidak memiliki konstruktor pemindahan dan tidak sepele bisa disalin.

Jerry Coffin
sumber
Draf kerja saat ini memungkinkan adanya perpindahan secara implisit dalam kondisi tertentu dan saya pikir resolusi tersebut sebagian besar membahas masalah Abrahams.
James McNellis
Saya tidak yakin saya mengerti langkah apa yang mungkin rusak dalam contoh antara Tweak 2 dan Tweak 3. Bisakah Anda menjelaskannya?
Matthieu M.
@ Matthieu M .: baik Tweak 2 dan Tweak 3 rusak, dan sebenarnya dengan cara yang sangat mirip. Di Tweak 2, ada anggota pribadi dengan invarian yang dapat dipatahkan oleh ctor yang bergerak. Di Tweak 3, kelas tidak memiliki anggota pribadi itu sendiri , tetapi karena menggunakan warisan pribadi, anggota pangkalan publik dan dilindungi menjadi anggota pribadi dari turunan, yang mengarah ke masalah yang sama.
Jerry Coffin
1
Saya tidak benar-benar mengerti bagaimana konstruktor pemindahan akan memecahkan invarian kelas Tweak2. Saya kira itu ada hubungannya dengan fakta bahwa Numberakan dipindahkan dan vectorakan disalin ... tapi saya tidak yakin: / Saya mengerti masalahnya akan menurun Tweak3.
Matthieu M.
Tautan yang Anda berikan sepertinya sudah mati?
Serigala
8

(untuk saat ini, saya sedang mengerjakan makro bodoh ...)

Ya, saya pergi ke rute itu juga. Berikut makro Anda:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

</s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> </s> orang </s>

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(Saya telah menghapus komentar sebenarnya, yang panjang dan dokumenter.)

Anda menentukan basis dan / atau anggota di kelas Anda sebagai daftar praprosesor, misalnya:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

Dan keluarlah seorang konstruktor bergerak dan operator tugas bergerak.

(Sebagai tambahan, jika ada yang tahu bagaimana saya bisa menggabungkan detail ke dalam satu makro, itu akan menjadi besar.)

GManNickG
sumber
Terima kasih banyak, milik saya sangat mirip, kecuali saya harus memberikan jumlah variabel anggota sebagai argumen (yang sangat menyebalkan).
Viktor Sehr
1
@Viktor: Tidak masalah. Jika belum terlambat, saya pikir Anda harus menandai salah satu jawaban lain sebagai diterima. Milik saya lebih merupakan "omong-omong, ini caranya" dan bukan jawaban untuk pertanyaan Anda yang sebenarnya.
GManNickG
1
Jika saya membaca makro Anda dengan benar, maka segera setelah kompilator Anda menerapkan anggota pemindahan default, contoh Anda di atas tidak dapat disalin. Pembuatan anggota salinan secara implisit dihambat ketika ada anggota pemindahan yang dinyatakan secara eksplisit.
Howard Hinnant
@ Howard: Tidak apa-apa, ini adalah solusi sementara sampai saat itu. :)
GManNickG
GMan: Makro ini menambahkan moveconstructor \ assign jika Anda memiliki fungsi swap:
Viktor Sehr
4

VS2010 tidak melakukannya karena mereka bukan Standar pada saat implementasi.

Anak anjing
sumber