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) ) ) isboost :: 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.

questionAnswers(0)

yourAnswerToTheQuestion