Apakah GCC9 menghindari keadaan std :: varian yang tidak bernilai diizinkan?

14

Saya baru-baru ini mengikuti diskusi Reddit yang mengarah ke perbandingan yang bagus dari std::visitoptimasi di kompiler. Saya perhatikan hal berikut: https://godbolt.org/z/D2Q5ED

Baik GCC9 dan Clang9 (saya kira mereka berbagi stdlib yang sama) tidak menghasilkan kode untuk memeriksa dan melempar pengecualian yang tidak bernilai ketika semua jenis memenuhi beberapa persyaratan. Ini mengarah ke codegen cara yang lebih baik, maka saya mengangkat masalah dengan MSVC STL dan disajikan dengan kode ini:

template <class T>
struct valueless_hack {
  struct tag {};
  operator T() const { throw tag{}; }
};

template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
  try { v.emplace<0>(valueless_hack<First>()); }
  catch(typename valueless_hack<First>::tag const&) {}
}

Klaimnya adalah, bahwa ini membuat varian tidak bernilai, dan membaca dokumen itu harus:

Pertama, hancurkan nilai yang saat ini terkandung (jika ada). Kemudian langsung-menginisialisasi nilai yang terkandung seolah-olah membangun nilai tipe T_Idengan argumen std::forward<Args>(args)....Jika pengecualian dilemparkan, *thisdapat menjadi valueless_by_exception.

Apa yang saya tidak mengerti: Mengapa itu dinyatakan sebagai "mungkin"? Apakah sah untuk tetap di negara lama jika seluruh operasi dilemparkan? Karena inilah yang dilakukan GCC:

  // For suitably-small, trivially copyable types we can create temporaries
  // on the stack and then memcpy them into place.
  template<typename _Tp>
    struct _Never_valueless_alt
    : __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
    { };

Dan kemudian (kondisional) melakukan sesuatu seperti:

T tmp  = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);

Oleh karena itu pada dasarnya itu menciptakan sementara, dan jika itu berhasil menyalin / memindahkannya ke tempat nyata.

IMO ini merupakan pelanggaran "Pertama, hancurkan nilai yang saat ini terkandung" sebagaimana dinyatakan oleh dokumen. Ketika saya membaca standar, maka setelah v.emplace(...)nilai saat ini dalam varian selalu dihancurkan dan tipe baru adalah tipe yang ditetapkan atau tidak berharga.

Saya mendapatkan bahwa kondisi is_trivially_copyablemengecualikan semua jenis yang memiliki destruktor yang dapat diamati. Jadi ini bisa juga sebagai: "varian as-if diinisialisasi ulang dengan nilai lama" atau lebih. Tetapi keadaan varian adalah efek yang dapat diamati. Jadi apakah standar memang memungkinkan, yang emplacetidak mengubah nilai saat ini?

Edit sebagai respons terhadap kutipan standar:

Kemudian menginisialisasi nilai yang terkandung seolah-olah langsung-non-daftar-inisialisasi nilai tipe TI dengan argumen std​::​forward<Args>(args)....

Apakah T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);benar-benar dihitung sebagai implementasi yang valid dari hal di atas? Apakah ini yang dimaksud dengan "seolah-olah"?

Api
sumber

Jawaban:

7

Saya pikir bagian penting dari standar adalah ini:

Dari https://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4 Pengubah

(...)

templat variant_alternative_t> & emplace (Args && ... args);

(...) Jika pengecualian dilemparkan selama inisialisasi nilai yang terkandung, varian mungkin tidak memiliki nilai

Dikatakan "mungkin" tidak "harus". Saya berharap ini disengaja untuk memungkinkan implementasi seperti yang digunakan oleh gcc.

Seperti yang Anda sebutkan sendiri, ini hanya mungkin jika penghancur semua alternatif sepele dan dengan demikian tidak dapat diobservasi karena menghancurkan nilai sebelumnya diperlukan.

Pertanyaan lanjutan:

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

Apakah T tmp {std :: forward (args) ...}; this-> value = std :: move (tmp); benar-benar dihitung sebagai implementasi yang valid dari hal di atas? Apakah ini yang dimaksud dengan "seolah-olah"?

Ya, karena untuk tipe yang dapat disalin sepele tidak ada cara untuk mendeteksi perbedaannya, sehingga implementasinya berperilaku seolah-olah nilainya diinisialisasi seperti yang dijelaskan. Ini tidak akan berfungsi jika jenisnya tidak dapat disalin secara sepele.

PaulR
sumber
Menarik. Saya memperbarui pertanyaan dengan permintaan tindak lanjut / klarifikasi. Rootnya adalah: Apakah salin / pindah diizinkan? Saya sangat bingung dengan might/maykata - kata karena standar tidak menyatakan apa alternatifnya.
Flamefire
Menerima ini untuk kutipan standar dan there is no way to detect the difference.
Flamefire
5

Jadi apakah standar memang memungkinkan, yang emplacetidak mengubah nilai saat ini?

Iya. emplaceharus memberikan jaminan dasar agar tidak bocor (yaitu, menghormati umur objek ketika konstruksi dan penghancuran menghasilkan efek samping yang dapat diamati), tetapi jika memungkinkan, diizinkan untuk memberikan jaminan yang kuat (yaitu, kondisi awal disimpan ketika operasi gagal).

variantdiperlukan untuk berperilaku serupa dengan serikat pekerja - alternatif dialokasikan di satu wilayah penyimpanan yang dialokasikan sesuai. Tidak diperbolehkan mengalokasikan memori dinamis. Oleh karena itu, perubahan tipe emplacetidak memiliki cara untuk menjaga objek asli tanpa memanggil konstruktor bergerak tambahan - ia harus menghancurkannya dan membangun objek baru sebagai pengganti objek tersebut. Jika konstruksi ini gagal, maka varian harus pergi ke keadaan tanpa nilai yang luar biasa. Ini mencegah hal-hal aneh seperti menghancurkan objek yang tidak ada.

Namun, untuk jenis kecil yang dapat disalin sepele, dimungkinkan untuk memberikan jaminan yang kuat tanpa terlalu banyak biaya tambahan (bahkan peningkatan kinerja untuk menghindari cek, dalam hal ini). Karena itu, implementasinya melakukannya. Ini sesuai standar: implementasi masih memberikan jaminan dasar seperti yang disyaratkan oleh standar, hanya dengan cara yang lebih ramah pengguna.

Edit sebagai respons terhadap kutipan standar:

Kemudian menginisialisasi nilai yang terkandung seolah-olah langsung-non-daftar-inisialisasi nilai tipe TI dengan argumen std​::​forward<Args>(args)....

Apakah T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);benar-benar dihitung sebagai implementasi yang valid dari hal di atas? Apakah ini yang dimaksud dengan "seolah-olah"?

Ya, jika pemindahan tugas tidak menghasilkan efek yang dapat diamati, yang merupakan kasus untuk jenis yang dapat disalin secara sepele.

LF
sumber
Saya sepenuhnya setuju dengan alasan logika. Saya hanya tidak yakin ini sebenarnya dalam standar? Bisakah Anda mendukung ini dengan apa saja?
Flamefire
@ Flamefire Hmm ... Secara umum, fungsi standar memberikan jaminan dasar (kecuali ada yang salah dengan apa yang disediakan pengguna), dan std::varianttidak memiliki alasan untuk memecahnya. Saya setuju bahwa ini dapat dibuat lebih eksplisit dalam kata-kata standar, tetapi ini pada dasarnya bagaimana orang lain menjadi bagian dari pekerjaan standar perpustakaan. Dan FYI, P0088 adalah proposal awal.
LF
Terima kasih. Ada spesifikasi yang lebih eksplisit di dalamnya: if an exception is thrown during the call toT’s constructor, valid()will be false;Jadi itu melarang "optimasi" ini
Flamefire
Iya. Spesifikasi emplacedalam P0088 di bawahException safety
Flamefire
@ Firefire Tampaknya ada perbedaan antara proposal asli dan versi yang dipilih. Versi terakhir berubah menjadi kata "boleh".
LF