Osana C ++ -opiskelua, painottaen erityisesti C ++ 11: tä, halusin toteuttaa Boost-muunnoksen (joka sijaitsee täällä ). Koodini on saatavilla osoitteessa variant.hpp , nykyinen versio on annettu alla .
Miten std::aligned_storage
voidaan käyttää kannettavasti? Nykyinen ratkaisuni käyttää todennäköisesti static_cast
-toimintoa ei-kannettavana, vaikka jos se on kannettava, kyseinen tieto olisi erittäin arvokasta. Erityinen koodi on samanlainen kuin *static_cast<T*>(static_cast<void*>(&value))
, value
-tyypille typename std::aligned_storage<...>::type
(jossa ...
ei ole tarkoitettu osoittamaan variaattisia malleja).
Käytän jonkin verran static_assert
. Olisiko tässä erityisessä käytössä SFINAE parempi? Ymmärrän, että SFINAE: lla voidaan karsia ylikuormituksia toimivien toimintojen joukosta, mutta missä käytän static_assert
I oletetaan siellä olisi vain yksi käyttökelpoinen toiminto, vaikka löytäisin arvokkaita esimerkkejä tapauksista, joissa on enemmän kuin yksi toimiva toiminto.
Käytin paljon std::forward
. Onko mahdollista päästä toimeen pienemmillä käyttötarkoituksilla?
Käytin std::enable_if
-sovellusta yhdessä konstruktorin ylikuormituksista varmistaakseni, että sitä käytettäisiin vain liikkeessä on tarkoitettu (katso variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)
). Ilman molempia enable_if
-tietoja tätä ohjainta käytettäisiin, kun kopiorakentaja variant(variant const&)
on tarkoitettu, vaikka edellinen johtaa mahdollinen kääntäjävirhe. Onko olemassa parempaa tapaa pakottaa tämä käyttäytyminen? Yksi kokeilemani ratkaisu oli sisällyttää variant(variant&)
ylikuormituksena, joka vain viivyttää variant(variant const& rhs)
– se valitaan yli variant(U&&)
, kun taas variant(U&&)
on parempi kuin variant(variant const&)
ylikuormitussäännöillä. Mikä on yleinen paras käytäntö, kun T&&
käytetään joillekin äskettäin käyttöönotetuille T
, kun siirtosemantiikka on tarkoitettu yleisen viitteen sijaan?
Minun on vielä lisättävä multivisitoreja, vaikka minulla on tässä yleisiä ongelmia (käyttäen variadic-malleja). Jotain mielenkiintoista, joka tuli esiin luokan variant
käyttöönotossa, oli implisiittisiä konversioita variant
-ryhmien välillä, jotka liittyivät vain malliarvojen järjestämiseen uudelleen tai joissa arvon argumentit ovat rvalue-mallin argumenttien yläjoukko.
Kaikkia kommentteja, kysymyksiä tai neuvoja arvostetaan suuresti.
#ifndef WART_VARIANT_HPP #define WART_VARIANT_HPP #include <type_traits> #include <utility> #include "math.hpp" namespace wart { template <typename... T> class variant; namespace detail { namespace variant { template <typename... T> using variant = wart::variant<T...>; template <typename T> using is_movable = typename std::integral_constant <bool, std::is_rvalue_reference<T&&>::value && !std::is_const<T>::value>; template <typename T, typename U = void> using enable_if_movable = std::enable_if<is_movable<T>::value, U>; template <typename... Types> using union_storage = typename std::aligned_storage <math::max_constant<std::size_t, sizeof(Types)...>::value, math::lcm_constant<std::size_t, std::alignment_of<Types>::value...>::value>::type; template <typename... Types> using union_storage_t = typename union_storage<Types...>::type; template <typename Elem, typename... List> struct elem; template <typename Head, typename... Tail> struct elem<Head, Head, Tail...>: std::true_type {}; template <typename Elem, typename Head, typename... Tail> struct elem<Elem, Head, Tail...>: elem<Elem, Tail...>::type {}; template <typename Elem> struct elem<Elem>: std::false_type {}; template <typename Elem, typename... List> struct elem_index; template <typename Head, typename... Tail> struct elem_index<Head, Head, Tail...>: std::integral_constant<int, 0> {}; template <typename Elem, typename Head, typename... Tail> struct elem_index<Elem, Head, Tail...>: std::integral_constant<int, elem_index<Elem, Tail...>::value + 1> {}; template <bool... List> struct all; template <> struct all<>: std::true_type {}; template <bool... Tail> struct all<true, Tail...>: all<Tail...>::type {}; template <bool... Tail> struct all<false, Tail...>: std::false_type {}; template <typename Elem, typename... List> using enable_if_elem = std::enable_if<elem<Elem, List...>::value>; template <typename F, typename... ArgTypes> using common_result_of = std::common_type<typename std::result_of<F(ArgTypes)>::type...>; struct destroy { template <typename T> void operator()(T&& value) { using type = typename std::remove_reference<T>::type; std::forward<T>(value).~type(); } }; struct copy_construct { void* storage; template <typename T> void operator()(T const& value) { new (storage) T(value); } }; template <typename... T> struct copy_construct_index { void* storage; template <typename U> int operator()(U const& value) { new (storage) U(value); return elem_index<U, T...>::value; } }; struct move_construct { void* storage; template <typename T> typename enable_if_movable<T>::type operator()(T&& value) { new (storage) T(std::move(value)); } }; template <typename... T> struct move_construct_index { void* storage; template <typename U> typename enable_if_movable<U, int>::type operator()(U&& value) { new (storage) U(std::move(value)); return elem_index<U, T...>::value; } }; struct copy_assign { void* storage; template <typename T> void operator()(T const& value) { *static_cast<T*>(storage) = value; } }; template <typename... T> struct copy_assign_reindex { variant<T...>& variant; template <typename U> void operator()(U const& value) { if (variant.which_ == elem_index<U, T...>::value) { *static_cast<U*>(static_cast<void*>(&variant.storage_)) = value; } else { variant.accept(destroy{}); new (&variant.storage_) U(value); variant.which_ = elem_index<U, T...>::value; } } }; struct move_assign { void* storage; template <typename T> typename enable_if_movable<T>::type operator()(T&& value) { *static_cast<T*>(storage) = std::move(value); } }; template <typename... T> struct move_assign_reindex { variant<T...>& variant; template <typename U> typename enable_if_movable<U>::type operator()(U&& value) { if (variant.which_ == elem_index<U, T...>::value) { *static_cast<U*>(static_cast<void*>(&variant.storage_)) = std::move(value); } else { variant.accept(destroy{}); new (&variant.storage_) U(std::move(value)); variant.which_ = elem_index<U, T...>::value; } } }; } } template <typename... T> class variant { int which_; detail::variant::union_storage_t<T...> storage_; public: template <typename F> using result_of = detail::variant::common_result_of<F, T...>; template <typename F> using result_of_t = typename result_of<F>::type; template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr): which_{detail::variant::elem_index<U, T...>::value} { new (&storage_) U(value); } template <typename U> variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr): which_{detail::variant::elem_index<U, T...>::value} { new (&storage_) U(std::move(value)); } variant(variant const& rhs): which_{rhs.which_} { rhs.accept(detail::variant::copy_construct{&storage_}); } template <typename... U> variant(variant<U...> const& rhs, typename std::enable_if< detail::variant::all<detail::variant::elem<U, T...>::value...>::value >::type* = nullptr): which_{rhs.accept(detail::variant::copy_construct_index<T...>{&storage_})} {} variant(variant&& rhs): which_{rhs.which_} { std::move(rhs).accept(detail::variant::move_construct{&storage_}); } template <typename... U> variant(variant<U...>&& rhs, typename std::enable_if< detail::variant::all<detail::variant::elem<U, T...>::value...>::value >::type* = nullptr): which_{std::move(rhs).accept(detail::variant::move_construct_index<T...>{&storage_})} {} ~variant() { accept(detail::variant::destroy{}); } variant& operator=(variant const& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_copy_constructible<T>::value...>::value, "all template arguments T must be nothrow copy constructible in class template variant"); if (this == &rhs) { return *this; } if (which_ == rhs.which_) { rhs.accept(copy_assign{&storage_}); } else { accept(destroy{}); rhs.accept(copy_construct{&storage_}); which_ = rhs.which_; } return *this; } template <typename... U> variant& operator=(variant<U...> const& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_copy_constructible<T>::value...>::value, "all template arguments T must be nothrow copy constructible in class template variant"); rhs.accept(copy_assign_reindex<T...>{*this}); return *this; } variant& operator=(variant&& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_move_constructible<T>::value...>::value, "all template arguments T must be nothrow move constructible in class template variant"); if (this == &rhs) { return *this; } if (which_ == rhs.which_) { std::move(rhs).accept(move_assign{&storage_}); } else { accept(detail::variant::destroy{}); std::move(rhs).accept(move_construct{&storage_}); which_ = rhs.which_; } return *this; } template <typename... U> variant& operator=(variant<U...>&& rhs) & { using namespace detail::variant; static_assert(all<std::is_nothrow_copy_constructible<T>::value...>::value, "all template arguments T must be nothrow copy constructible in class template variant"); std::move(rhs).accept(move_assign_reindex<T...>{*this}); return *this; } template <typename F> result_of_t<F> accept(F&& f) const& { using namespace detail::variant; using call = result_of_t<F&&> (*)(F&& f, union_storage_t<T...> const&); static call calls[] { [](F&& f, union_storage_t<T...> const& value) { return std::forward<F>(f)(*static_cast<T const*>(static_cast<void const*>(&value))); }... }; return calls[which_](std::forward<F>(f), storage_); } template <typename F> result_of_t<F> accept(F&& f) & { using namespace detail::variant; using call = result_of_t<F&&> (*)(F&& f, union_storage_t<T...>&); static call calls[] { [](F&& f, union_storage_t<T...>& value) { return std::forward<F>(f)(*static_cast<T*>(static_cast<void*>(&value))); }... }; return calls[which_](std::forward<F>(f), storage_); } template <typename F> result_of_t<F> accept(F&& f) && { using namespace detail::variant; using call = result_of_t<F> (*)(F&& f, union_storage_t<T...>&&); static call calls[] { [](F&& f, union_storage_t<T...>&& value) { return std::forward<F>(f)(std::move(*static_cast<T*>(static_cast<void*>(&value)))); }... }; return calls[which_](std::forward<F>(f), std::move(storage_)); } friend struct detail::variant::copy_assign_reindex<T...>; friend struct detail::variant::move_assign_reindex<T...>; }; } #endif
Kommentit
Vastaus
Täällä on paljon, joten aion jakaa arvosteluni palasiksi. Haluan aloittaa keskittymällä vain metafunktio-osioon. Metafunktiot voivat olla lyhyitä, mutta ne ovat erittäin voimakkaita ja tärkeitä oikeaksi pääsemiseksi – mutta niiden oikeellisuuden ja hyödyllisyyden kannalta.
Aluksi:
template <typename T> using is_movable = typename std::integral_constant <bool, std::is_rvalue_reference<T&&>::value && !std::is_const<T>::value>; template <typename T, typename U = void> using enable_if_movable = std::enable_if<is_movable<T>::value, U>;
Ensimmäinen on yksinkertaisesti väärä. Käytät tätä metatoimintoa tarkistaaksesi, onko tyyppi siirrettävissä (move_construct
) … mutta teet tämän tarkistamalla vain, onko se ei arvoarvo tai const
. Et todellakaan tarkista mitään, joka liittyy liikkuvaan rakenteeseen. Se, että jokin on arvon viitearvo, ei tarkoita, että voit siirtyä siitä. Ja se, että jokin on arvoarvo, ei tarkoita sitä, ettet voi. Harkitse kahta yksinkertaista luokkaa:
struct No { A(A&& ) = delete; }; struct Yes { };
Kuten nimestäkin käy ilmi, No
ei ole siirrettävissä. Metafunktiosi sanoo sen olevan. Myös Yes&
on siirrettävissä, mutta metafunktiosi sanoo ei.
Oikea toteutus olisi yksinkertaisen tyypin ominaisuuden käyttäminen std::is_move_constructible
.
Toiseksi siellä oleva alias on kyseenalainen. Tyypillisesti käytämme aliaksia välttää kirjoita typename ::type
-riste. Et tee niin, ja tuloksena oleva puhelu ei ole niin ytimekäs. Vertaa:
typename enable_if_movable<T>::type // yours with alias std::enable_if_t<is_moveable<T>::value> // just using enable_if without alias std::enable_if_t<std::is_move_constructible<T>::value> // just stds
Haluaisin edellisen version henkilökohtaisesti. Huomaa, että käytän C ++ 14-aliasta täällä. Jos sinulla ei ole C ++ 14-kääntäjää, on ehdottomasti sen arvoista aloittaa metatoimintokirjasto kaikilla. Heidän täytyy vain kirjoittaa:
template <bool B, typename T = void> using enable_if_t = typename std::enable_if<B, T>::type;
Siirtyminen:
template <typename Elem, typename... List> struct elem;
Ei ole siten, että kuka tahansa tietää mitä elem
tekee täällä. En t ennen kuin luin toteutuksen. Paljon parempi nimi tälle olisi contains
. Mutta palaan käyttöönottoon hetken kuluttua.
Aloitetaan ensin:
template <bool... List> struct all;
all
on erittäin hyödyllinen. Samoin ovat sen lähisukulaiset any
ja none
. Tapa, jolla kirjoitit all
, on hieno ja toimii, mutta ei tee kaksi muuta on helpompi kirjoittaa. Hyvä tapa kirjoittaa nämä on käyttää @Columbo ”s bool_pack
-temppua:
template <bool...> struct bool_pack; template <bool f, bool... bs> using all_same = std::is_same<bool_pack<f, bs...>, bool_pack<bs..., f>>;
Se” s vain auttajasi. Voit käyttää sitä toteuttamaan kaikki muut helposti:
template <bool... bs> using all_of = all_same<true, bs...>; template <bool... bs> using none_of = all_same<false, bs...>; template <bool... bs> using any_of = std::integral_constant<bool, !none_of<bs...>::value>;
Ja kun se on saatu, voimme toteuttaa uudelleen contains
yksirivisenä:
template <typename Elem, typename... List> using contains = any_of<std::is_same<Elem, List>::value...>;
Aivan kuten aiemmin, en näe arvoa enable_if_elem
Ja common_result_of
tulisi ottaa tyyppi , ei pelkästään tuottaa metafunktio:
template <typename F, typename... ArgTypes> using common_result_of = std::common_type_t<std::result_of_t<F(ArgTypes)>::...>;
Vaikka on helpompaa vain kiinnittää se variant
-sisältöön:
// no need to use anything in detail::, unless you need to // write your own aliases for common_type_t and result_of_t template <typename F> using result_of = std::common_type_t<std::result_of_t<F(T)>...>;
Nyt käyttöön . Käytät koko ajan näitä metafunktioita palautustyypissä:
template <typename T> typename enable_if_movable<T>::type operator()(T&& value);
Tai näennäisosoittimena:
template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr)
Mutta molemmissa tapauksissa minusta on paljon helpompaa jäsentää monimutkaisia mallilausekkeita, jos asetat SFINAE-logiikan nimeämättömäksi lopulliseksi malliparametriksi:
template <typename T, typename = std::enable_if_t<std::is_move_constructible<T>::value> > void operator()(T&& value); template <typename U, typename = std::enable_if_t<contains<U, T...>::value> > variant(U const& value);
Johdonmukaisuus auttaa myös ymmärtämistä. Nuken osoittimen argumentti on sekava hakkeroinnin jäännös C ++ 03: sta. Sitä ei enää tarvita. Varsinkin kun tarvitset kahta nuken osoitinta:
template <typename U, typename = std::enable_if_t<contains<U, T...>::value && std::is_move_constructible<U>::value> > variant(U&& value);
Tämän kaverin sivuhuomautus. ei todellakaan tee mitä haluat. Tämä ei ole mielivaltainen arvoarvo – se on edelleenlähetysviite. Itse asiassa voimme jopa yhdistää nämä kaksi rakentajaa yhdellä kertaa:
template <typename U, typename V = std::remove_reference_t<U>, typename = std::enable_if_t<contains<V, T...>::value && std::is_constructible<V, U&&>::value> > variant(U&& value) : which_{elem_index<V, T...>::value} { new (&storage_) V(std::forward<U>(value)); }
Huomaa, että tämä ratkaisee myös toisen ongelman koodissasi – nimittäin että et koskaan tarkista jos luokka on kopioitavissa. Entä jos haluaisit liittää varianttiin jotain unique_ptr
. Siirrä rakennettavissa olevaa, mutta älä kopioi rakennettavaa – mutta et ole koskaan tarkistanut sitä koodissasi. Tärkeää – muuten käyttäjät saisivat vain salaisia virheilmoituksia.
Luulen, että tämä lopettaa metafunktio-osan. Kirjoitan katsauksen itsestäni variant
hieman myöhemmin.Toivottavasti löydät tästä hyödyllisen.
std::aligned_storage
. Neuvot liittyväntype
-tuotteen kannettavasta käytöstä olisi erittäin hyödyllistä. Jos ” työn ” avulla tarkoitat ” tyydyttääkö se alkuperäisen tarkoituksen? ”, vastaus on kyllä, vaikka haluaisin kovasti tyylillisiä ja parhaita käytäntöjä koskevia neuvoja.std::forward
näyttää hyvältä, en voi ’ nähdä paikkaa, jossa et voisi käyttää sitä. Pidän parempanastatic_assert
, koska sen avulla voit antaa parempia virheilmoituksia. Siirrettävyys on mahdollisesti ongelma, koska ei ole takeita siitä, että kaikilla osoittimilla on sama raja (jotenvoid *
jaT*
voi olla erilaisia vaatimuksia ), mutta tämä olisi nykyään hyvin epätavallista. En tiedä ’ en tiedä kuinka vauhti kiertää tämän, tai jos he vain sivuuttavat sen.std::tuple
, muttaunion
.is_movable
jaenable_if_movable
, esimerkiksi), jotka voivat mahdollisesti asua muualla. Valitettavasti C ++ -tarkastajien lukumäärä tältä sivulta, jolla on tarvittavaa tietoa, jotta saat hyvän palautteen tästä, on todennäköisesti yksinumeroinen, koska koodi on melko monimutkainen.