Semantyka dla zawijanych obiektów: domyślnie odniesienie / wartość przez std :: move / std :: ref
W ostatnich czasach często używam naturalnego idiomu, który „odkryłem” w C ++ 11, a mianowicie, że zawinięty obiekt może automatycznie utrzymywać odniesienie, gdy jest to możliwe. Głównym pytaniem będzie tutaj porównanie z zachowaniem tego „idiomu” do innych zachowań w standardzie (patrz poniżej).
Na przykład:
template<class T>
struct wrap{
T t;
};
template<class T> wrap<T> make_wrap(T&& t){
return wrap{std::forward<T>(t)};
}
W ten sposób dla kodu
double a = 3.14
double const c = 3.14
Dostaję,
typeid( make_wrap(3.14) ) --> wrap<double>
typeid( make_wrap(a) ) --> wrap<double&>
typeid( make_wrap(c) ) --> wrap<double const&>
które, jeśli jestem ostrożny (ze zwisającymi odniesieniami), mogę poradzić sobie całkiem nieźle. A jeśli chcęuniknąć referencje robię:
typeid( make_wrap(std::move(a)) ) --> wrap<double> // noref
typeid( make_wrap(std::move(c)) ) --> wrap<double const> // noref
Tak więc to zachowanie wydaje się naturalne w C ++ 11.
Potem wróciłem dostd::pair
istd::make_pair
i jakoś spodziewałem się, że użyli tego nowego, pozornie naturalnego zachowania, ale najwyraźniej zachowanie jest „bardziej tradycyjne”. Na przykład:
typeid( std::make_pair(3.14, 3.14) ) --> std::pair<double, double>
typeid( std::make_pair(a, a) ) --> std::pair<double, double> // noref
typeid( std::make_pair(c, c) ) --> std::pair<double, double> // noref
idla referencje:
typeid( std::make_pair(std::ref(a), std::ref(a) ) ) --> std::pair<double&, double&> // ref
typeid( std::make_pair(std::ref(c), std::ref(c) ) ) --> std::pair<double const&, double const&> // const ref
Jest to udokumentowane tutaj:http://en.cppreference.com/w/cpp/utility/pair/make_pair
Jak widzisz, dwa zachowania są w pewnym sensie „przeciwne”std::ref
jest uzupełnieniemstd::move
. Oba zachowania są jednakowo elastyczne na końcu, ale wydaje mi się, żestd::make_pair
zachowanie jest trudniejsze do wdrożenia i utrzymania.
Pytanie brzmi:Czy obecne zachowaniestd::make_pair
domyślne odrzucanie odwołań tylko problem ze zgodnością wsteczną? ponieważ jakieś oczekiwania historyczne? czy istnieje głębszy powód, który nadal istnieje w C ++ 11?
Tak to wyglądastd::make_pair
zachowanie jest znacznie trudniejsze do wdrożenia, ponieważ wymaga specjalizacjistd::ref
(std::reference_wrapper
) istd::decay
a nawet wydaje się nienaturalny (w obecności „C ++ 11 move”). Jednocześnie, nawet jeśli zdecyduję się nadal używać pierwszego zachowania, obawiam się, że zachowanie będzie dość nieoczekiwane w stosunku do obecnych standardów, nawet w C ++ 11.
W rzeczywistości bardzo lubię pierwsze zachowanie, do tego stopnia, że eleganckim rozwiązaniem może być zmiana prefiksumake_something
na coś takiegoconstruct_something
w celu zaznaczenia różnicy w zachowaniu. (EDYTOWAĆ: jeden z komentarzy sugerowanych do obejrzeniastd::forward_as_tuple
, więc może być inna konwencja nazwforward_as_something
). Jeśli chodzi o nazewnictwo, sytuacja nie jest jednoznaczna, gdy pass-by-value, pass-by-ref jest mieszany w konstrukcji obiektu.
EDIT2: Jest to edycja tylko po to, by odpowiedzieć @ Yakkowi na możliwość „skopiowania” obiektu zawijania o różnych właściwościach ref / value. To nie jest część pytania i jest to tylko kod eksperymentalny:
template<class T>
struct wrap{
T t;
// "generalized constructor"? // I could be overlooking something important here
template<class T1 = T> wrap(wrap<T1> const& w) : t(std::move(w.t)){}
wrap(T&& t_) : t(std::move(t)){} // unfortunately I now have to define an explicit constructor
};
Wydaje się, że pozwala mi to kopiować między niepowiązanymi typamiwrap<T&>
iwrap<T>
:
auto mw = make_wrap(a);
wrap<double const&> copy0 =mw;
wrap<double&> copy1 = mw; //would be an error if `a` is `const double`, ok
wrap<double> copy2 = mw;
EDIT3: Ta edycja ma na celu dodanie konkretnego przykładu, w którym tradycyjne dedukcje odniesienia mogą zawieść, zależą od „protokołu”. Przykład opiera się na wykorzystaniu Boost.Fusion.
Odkryłem, jak bardzo niejawna konwersja z odniesienia do wartości może zależeć od konwencji. Na przykład dobry stary Boost.Fusion jest zgodny z konwencją STL
Funkcje generacji Fusion (np. Make_list) domyślnie przechowują typy elementów jako zwykłe typy niereferencyjne.
Jednak w przypadku Fusion polegał on na dokładnym „typie”, który oznacza odniesienieboost::ref
iw przypadkumake_pair
jest...std::ref
, zupełnie niepowiązana klasa. Tak więc, obecnie dane
double a;
typboost::fusion::make_vector(5., a )
jestboost::fusion::vector2<double, double>
. Ok dobrze.
I rodzajboost::fusion::make_vector(5., boost::ref(a) ) ) is
boost :: fusion :: vector2`. Ok, jak udokumentowano.
Jednak niespodzianka, ponieważ Boost.Fusion nie został napisany z C ++ 11 STL ma na myśli:boost::fusion::make_vector(5., std::ref(a) ) )
jest typuboost::fusion::vector2<double, std::reference_wrapper<double const> >
. Niespodzianka!
Ta sekcja miała pokazać, że obecne zachowanie STL zależy od protokołu (np. Jakiej klasy użyć do oznaczania odniesień), podczas gdy druga (co nazwałam „naturalnym” zachowaniem) używającstd::move
(lub dokładniej odlewanie wartości rvalue) nie zależy od protokołu, ale jest bardziej rodzimy dla (bieżącego) języka.