Als onderdeel van het leren van C ++, met speciale nadruk op C ++ 11, wilde ik het equivalent van Boosts Variant (gelokaliseerd hier ). Mijn code is beschikbaar op variant.hpp , met de huidige versie hieronder .
Hoe kan std::aligned_storage
draagbaar worden gebruikt? Mijn huidige oplossing maakt waarschijnlijk niet-draagbaar gebruik van static_cast
als het draagbaar is, zou die informatie erg waardevol zijn. De specifieke code is vergelijkbaar met *static_cast<T*>(static_cast<void*>(&value))
, voor value
van het type typename std::aligned_storage<...>::type
(waarbij ...
niet bedoeld is om variadische sjablonen aan te duiden).
Ik maak enig gebruik van static_assert
. Zou SFINAE bij dit specifieke gebruik beter zijn? Ik begrijp dat SFINAE kan worden gebruikt om overbelastingen uit de set van levensvatbare functies te verwijderen, maar waar ik static_assert
gebruik, neem daar aan zou slechts één levensvatbare functie zijn, hoewel ik waardevolle voorbeelden zou vinden van gevallen waarin er meer dan één levensvatbare functie is.
Ik heb veel gebruik gemaakt van std::forward
. Is het mogelijk om rond te komen met minder gebruik?
Ik heb std::enable_if
gebruikt bij een van de constructor-overloads om ervoor te zorgen dat het alleen zou worden gebruikt bij een verplaatsing is bedoeld (zie variant(U&& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr, typename detail::variant::enable_if_movable<U>::type* = nullptr)
). Zonder beide enable_if
s, zou deze constructor worden gebruikt wanneer de kopieerconstructor variant(variant const&)
in plaats daarvan bedoeld is, ook al resulteert de eerste in een eventuele compileerfout. Is er een betere manier om dit gedrag af te dwingen? Een oplossing die ik heb geprobeerd, was variant(variant&)
opnemen als een overbelasting die zich gewoon verplaatst naar variant(variant const& rhs)
– het zou worden geselecteerd via variant(U&&)
, terwijl variant(U&&)
de voorkeur heeft boven variant(variant const&)
door de overbelastingsregels. Wat is de algemene beste praktijk bij het gebruik van T&&
voor sommige nieuw geïntroduceerde T
wanneer verplaatsingssemantiek bedoeld is in plaats van een universele referentie?
Ik moet nog steeds multivisitors toevoegen, hoewel ik hier in het algemeen wat problemen mee heb (met behulp van variadische sjablonen). Iets interessants dat naar voren kwam bij het implementeren van de variant
-klasse waren impliciete conversies tussen variant
s waarbij alleen de sjabloonargumenten moesten worden herschikt of waarbij de lvalue-sjabloon argumenten zijn een superset van de rvalue-sjabloonargumenten.
Alle opmerkingen, vragen of advies worden zeer op prijs gesteld.
#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
Reacties
Antwoord
Er is hier veel, dus ik ga mijn recensie opsplitsen in stukjes. Ik wil beginnen door me alleen te concentreren op de metafunctie-sectie. Metafuncties zijn misschien kort, maar ze “zijn erg krachtig en belangrijk om goed te krijgen – maar dan in termen van correctheid en bruikbaarheid.
Om te beginnen:
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>;
De eerste is gewoon fout. Je gebruikt deze metafunctie om te controleren of een type verplaatsbaar is (in move_construct
) … maar je doet dit door gewoon te controleren of het geen lvalue-referentie is en ook geen const
. Je controleert eigenlijk niets met betrekking tot verplaatsingsconstructie. Alleen omdat iets een waardereferentie is, wil nog niet zeggen dat je ervan kunt vertrekken. En alleen omdat iets een waarde-referentie is, wil dat nog niet zeggen dat je dat niet kunt. Beschouw twee eenvoudige klassen:
struct No { A(A&& ) = delete; }; struct Yes { };
Zoals de naam al doet vermoeden, No
is niet verplaatsbaar. Uw metafunctie zegt van wel. Ook Yes&
is verplaatsbaar, maar uw metafunctie zegt nee.
De juiste implementatie zou zijn om gewoon het standaardtype-kenmerk te gebruiken std::is_move_constructible
.
Ten tweede is de alias daar twijfelachtig. Meestal gebruiken we aliassen om te vermijden schrijf de typename ::type
cruft. Dat doe je niet, en de resulterende oproep is niet veel beknopter. Vergelijk:
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
Persoonlijk zou ik de laatste versie verkiezen. Merk op dat ik hier de alias C ++ 14 gebruik. Als je geen C ++ 14 compiler hebt, is het absoluut de moeite waard om je metafunctiebibliotheek met al deze bestanden te starten. Ze zijn gewoon om te schrijven:
template <bool B, typename T = void> using enable_if_t = typename std::enable_if<B, T>::type;
Doorgaan naar:
template <typename Elem, typename... List> struct elem;
Er is geen zo dat iedereen weet wat elem
hier doet. Ik deed het pas toen ik de implementatie had gelezen. Een veel betere naam hiervoor zou contains
zijn. Maar ik zal zo weer verder gaan met de implementatie.
Laten we eerst beginnen met:
template <bool... List> struct all;
all
is super handig. Net als zijn naaste familieleden any
en none
. De manier waarop je all
schreef, is prima en werkt, maar maakt het niet het is gemakkelijker om de andere twee te schrijven. Een goede manier om deze op te schrijven is om @Columbo “s bool_pack
truc te gebruiken:
template <bool...> struct bool_pack; template <bool f, bool... bs> using all_same = std::is_same<bool_pack<f, bs...>, bool_pack<bs..., f>>;
Dat” s gewoon je helper. U kunt dat gebruiken om de rest gemakkelijk te implementeren:
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>;
En zodra we dat hebben, kunnen we contains
als een oneliner:
template <typename Elem, typename... List> using contains = any_of<std::is_same<Elem, List>::value...>;
Net als voorheen zie ik de waarde niet in enable_if_elem
. En common_result_of
zou het type moeten aannemen, niet alleen de metafunctie:
template <typename F, typename... ArgTypes> using common_result_of = std::common_type_t<std::result_of_t<F(ArgTypes)>::...>;
Hoewel het “beter leesbaar is om dat gewoon in uw variant
zelf te plakken:
// 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)>...>;
Nu naar het gebruik . Je gebruikt deze metafuncties overal in het retourtype:
template <typename T> typename enable_if_movable<T>::type operator()(T&& value);
Of als een dummy pointer:
template <typename U> variant(U const& value, typename detail::variant::enable_if_elem<U, T...>::type* = nullptr)
Maar in beide gevallen vind ik het een stuk gemakkelijker om complexe sjabloonuitdrukkingen te ontleden als je de SFINAE-logica als een naamloze laatste sjabloonparameter plaatst:
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);
De consistentie helpt ook om te begrijpen. Het dummy pointer-argument is een verwarrende hack die is overgebleven van C ++ 03. Het is niet meer nodig. Vooral als je twee dummy-aanwijzers nodig hebt:
template <typename U, typename = std::enable_if_t<contains<U, T...>::value && std::is_move_constructible<U>::value> > variant(U&& value);
Kanttekening bij deze man. doet niet echt wat je wilt. Dit is geen willekeurige rvalue-referentie – het is een forwarding-referentie. In feite kunnen we de twee constructors hier zelfs in één keer combineren:
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)); }
Merk op dat dit ook een ander probleem met je code oplost – namelijk dat je nooit controleert als een klasse kopieerbaar is. Wat als u iets als unique_ptr
in uw variant wilt plakken? Verplaats bouwbaar, maar kopieer niet bouwbaar – maar dat heb je nooit gecontroleerd in je code. Belangrijk – anders zouden uw gebruikers cryptische foutmeldingen krijgen.
Ik denk dat hiermee het metafunctie-gedeelte is afgesloten. Ik zal een beetje later een recensie van variant
zelf schrijven.Ik hoop dat je dit nuttig vindt.
std::aligned_storage
gebruik. Advies over het draagbare gebruik van de bijbehorendetype
zou zeer nuttig zijn. Als door ” werk “, bedoel je ” voldoet het aan de oorspronkelijke bedoeling? “, het antwoord is ja, hoewel ik heel graag stilistisch advies en best practices zou willen.std::forward
ziet er goed uit, ik kan ‘ nergens zien waar je het niet kon gebruiken. Ik geef de voorkeur aanstatic_assert
omdat je hierdoor betere foutmeldingen kunt geven. Draagbaarheid is mogelijk een probleem, aangezien er geen garantie is dat alle verwijzingen op dezelfde grens zijn uitgelijnd (dusvoid *
enT*
kunnen verschillende vereisten hebben ), maar dit zou tegenwoordig zeer ongebruikelijk zijn. Ik ‘ weet niet hoe boost hier omheen komt, of dat ze het gewoon negeren.std::tuple
gebruikt, maar metunion
.is_movable
enenable_if_movable
, bijvoorbeeld) die mogelijk ergens anders zou kunnen wonen. Helaas bestaat het aantal C ++ – recensenten hier met de kennis die nodig is om u hierover goede feedback te geven waarschijnlijk in de enkele cijfers, aangezien de code vrij complex is.