Po co używać idealnie przekazywanej wartości (functor)?
C++11 (i C++14) wprowadza dodatkowe konstrukcje językowe i ulepszenia, które dotyczą programowania ogólnego. Należą do nich takie cechy jak;
- odniesienia do wartości R
- Reference
- Perfect forwarding
- Przenieś semantykę, różne szablony i inne
Przeglądałem wcześniejszy szkic specyfikacji C++14 (teraz z zaktualizowanym tekstem) i kod w przykładzie w §20.5.1, compile-time integer sekwencje , które uznałem za interesujące i osobliwe.
template<class F, class Tuple, std::size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...);
}
template<class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
using Indices = make_index_sequence<std::tuple_size<Tuple>::value>;
return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices());
}
Online tutaj [intseq.ogólne] / 2 .
Pytanie
- dlaczego funkcja
f
wapply_impl
była przekazywana, tzn. dlaczegostd::forward<F>(f)(std::get...
? - Dlaczego po prostu nie zastosować funkcji jako
f(std::get...
?
2 answers
W Skrócie...
TL;DR, chcesz zachować kategorię wartości (charakter r-wartość/l-wartość) functora, ponieważ może to wpłynąć na rozdzielczość przeciążenia , w szczególności członków kwalifikowanych ref .
Redukcja definicji funkcji
W związku z tym, że funkcja jest przekazywana, zmniejszyłem próbkę (i zmusiłem ją do kompilacji kompilatorem C++11) do;template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
return std::forward<F>(func)(std::forward<Args>(args)...);
}
I tworzymy drugi formularz, w którym zastępujemy std::forward(func)
Z just func
;
template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
return func(std::forward<Args>(args)...);
}
Ocena próbki
Ocena niektórych empirycznych dowodów na to, jak to się zachowuje (z zgodnymi kompilatorami) jest dobrym punktem wyjścia do oceny, dlaczego przykład kodu został napisany jako taki. Stąd dodatkowo zdefiniujemy funktor ogólny;
struct Functor1 {
int operator()(int id) const
{
std::cout << "Functor1 ... " << id << std::endl;
return id;
}
};
Próbka wstępna
Uruchom jakiś przykładowy kod;
int main()
{
Functor1 func1;
apply_impl_2(func1, 1);
apply_impl_2(Functor1(), 2);
apply_impl(func1, 3);
apply_impl(Functor1(), 4);
}
I wyjście jest zgodne z oczekiwaniami, niezależne od tego, czy użyto wartości r Functor1()
czy wartości l func
podczas wykonywania połączenia do apply_impl
i apply_impl_2
wywoływany jest przeciążony operator połączenia. Jest wywoływana zarówno dla wartości r, jak i wartości l. W C++03, to było wszystko, co masz, nie można przeciążać metod prętowych opartych na" R-value-ness "lub" l-value-Ness " obiektu.
Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
Functor1 ... 4
Próbki kwalifikowane Ref
Teraz musimy przeciążyć ten operator wywołania, aby rozciągnąć to a jeszcze trochę...
struct Functor2 {
int operator()(int id) const &
{
std::cout << "Functor2 &... " << id << std::endl;
return id;
}
int operator()(int id) &&
{
std::cout << "Functor2 &&... " << id << std::endl;
return id;
}
};
Uruchamiamy kolejny zestaw próbek;
int main()
{
Functor2 func2;
apply_impl_2(func2, 5);
apply_impl_2(Functor2(), 6);
apply_impl(func2, 7);
apply_impl(Functor2(), 8);
}
A wyjście to;
Functor2 &... 5
Functor2 &.. 6
Functor2 &.. 7
Functor2 &&... 8
Dyskusja
W przypadku apply_impl_2
(id
5 i 6), Wyjście nie jest tak, jak początkowo można było się spodziewać. W obu przypadkach wywoływana jest wartość L operator()
(wartość r w ogóle nie jest wywoływana). Można się było spodziewać, że ponieważ Functor2()
, wartość r jest używana do wywołania apply_impl_2
, wywołano by kwalifikowaną wartość R operator()
. func
, jako parametr nazwany do apply_impl_2
, jest odniesieniem do wartości r, ale ponieważ jest nazwany, sam jest wartością l. Dlatego wartość L operator()(int) const&
jest wywoływana zarówno w przypadku, gdy wartość L func2
jest argumentem, jak i wartość r Functor2()
jest używana jako argument.
W przypadku apply_impl
(id
7 Oraz 8)std::forward<F>(func)
zachowuje lub zachowuje charakter wartości r/wartości l z argumentu podanego dla func
. Dlatego wartość l kwalifikowana operator()(int) const&
jest wywoływana z wartością l func2
używaną jako argument, a wartość R kwalifikowana operator()(int)&&
, gdy wartość R Functor2()
jest używana jako argument. Tego zachowania można się było spodziewać.
Wnioski
Użycie std::forward
, poprzez doskonałe przekazywanie, zapewnia, że zachowamy naturę r-value/l-value oryginalnego argumentu dla func
. Zachowuje ich wartość Kategoria .
Jest to wymagane, std::forward
może i powinno być używane nie tylko do przekazywania argumentów do funkcji, ale także wtedy, gdy użycie argumentu jest wymagane, gdy należy zachować charakter r-wartość/l-wartość. Uwaga; są sytuacje, w których wartość r/L nie może lub nie powinna być zachowana, w takich sytuacjach std::forward
nie powinna być używana(patrz converse poniżej).
Pojawia się wiele przykładów, które nieumyślnie tracą r-value/l-value charakter argumentów poprzez pozornie niewinne użycie odniesienia wartości r.
Zawsze trudno było napisać dobrze zdefiniowany i brzmiący kod ogólny. Wraz z wprowadzeniem odniesień o wartości r, a w szczególności odwołań, stało się możliwe napisanie lepszego kodu generycznego, bardziej zwięzłego, ale musimy być coraz bardziej świadomi tego, jaka jest oryginalna natura podanych argumentów i upewnić się, że są one zachowane, gdy używamy ich w generycznym kodzie kod, który piszemy.
Pełny przykładowy kod można znaleźć tutaj
Corollary i converse
- następstwem pytania byłoby; biorąc pod uwagę załamanie odniesienia w funkcji szablonowej, w jaki sposób zachowuje się charakter wartości r/L argumentu? Odpowiedź-użyj
std::forward<T>(t)
. - Converse; czy
std::forward
rozwiąże wszystkie Twoje problemy "uniwersalnego odniesienia"? Nie, są przypadki, w których nie powinno być używane , takie jak przekazywanie wartości więcej niż raz.
Krótkie tło do perfect forwarding
Doskonałe przekierowanie może być dla niektórych nieznane, więc czym jest doskonałe przekierowanie ?
W skrócie, perfect forwarding służy do zapewnienia, że argument dostarczony do funkcji jest przekazywany (przekazywany) do innej funkcji o tej samej kategorii wartości (zasadniczo r-wartość vs.l-wartość), jak pierwotnie dostarczone. Jest zwykle używany z funkcjami szablonu, gdzie odniesienie / align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center /
W 2013 roku, w trakcie prezentacji going Native 2013, Scott Meyers podał następujący pseudo kod, aby wyjaśnić działaniestd::forward
(około 20 minut); {[56]]}
template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
if (is_lvalue_reference<T>::value) {
return param; // return type T&& collapses to T& in this case
}
else {
return move(param);
}
}
W języku C++11 istnieje kilka podstawowych konstrukcji językowych, które tworzą podstawy dla wielu z tego, co widzimy obecnie w programowaniu ogólnym:]}
- odwołanie
- rvalue references
- Move semantyka
std::forward
jest obecnie zamierzone w formule std::forward<T>
, zrozumienie jak działa std::forward
pomaga zrozumieć, dlaczego tak jest, a także pomaga w identyfikacji Nie-idiomatycznego lub nieprawidłowego użycia wartości R, załamania odniesienia i ilk.
Thomas Becker zapewnia miły, ale gęsty zapis na idealne przekierowanie problem i rozwiązanie .
Czym są kwalifikatory ref?
Ref-qualifiers (lvalue ref-qualifier &
i rvalue ref-qualifier &&
) są podobne do kwalifikatorów cv, ponieważ (ref-Qualifiers ) są używane podczas overload resolution do określenia, którą metodę wywołać. Zachowują się tak, jak byś tego oczekiwał; &
stosuje się do wartości LV, A &&
do wartości R. Uwaga: w przeciwieństwie do kwalifikacji cv, *this
pozostaje wyrażeniem wartości L.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:00:10
Oto praktyczny przykład.
struct concat {
std::vector<int> state;
std::vector<int> const& operator()(int x)&{
state.push_back(x);
return state;
}
std::vector<int> operator()(int x)&&{
state.push_back(x);
return std::move(state);
}
std::vector<int> const& operator()()&{ return state; }
std::vector<int> operator()()&&{ return std::move(state); }
};
Ten obiekt funkcji przyjmuje x
i łączy go z wewnętrznym std::vector
. Następnie zwraca std::vector
.
Jeśli jest oceniana w kontekście rvalue, to move
jest tymczasowa, w przeciwnym razie zwraca const&
do wektora wewnętrznego.
Teraz nazywamy apply
:
auto result = apply( concat{}, std::make_tuple(2) );
Ponieważ starannie przekazaliśmy obiekt funkcji, przydzielany jest tylko 1 std::vector
bufor. Jest po prostu przeniesiony do result
.
BEZ ostrożności przekierowanie, w końcu tworzymy wewnętrzny std::vector
i kopiujemy go do result
, a następnie odrzucamy wewnętrzny std::vector
.
Ponieważ operator()&&
wie, że obiekt funkcji powinien być traktowany jako wartość R, która ma zostać zniszczona, może wyrwać wnętrzności z obiektu funkcji podczas jego działania. operator()&
nie może tego zrobić.
Staranne wykorzystanie perfekcyjnego przekazywania obiektów funkcyjnych umożliwia taką optymalizację.
Zauważ jednak, że jest bardzo mało wykorzystania tej techniki "w dziczy" w tym momencie. Przeciążenie kwalifikowane rvalue jest niejasne i robi to operator()
moreso.
Mogłem łatwo zobaczyć przyszłe wersje C++ automatycznie używając stanu rvalue lambda do niejawnie move
jego przechwytywanych przez wartość danych w pewnych kontekstach.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-24 04:01:47